You said:
ChatGPT said:
Yes — and we’re off to a powerful start!
You've uploaded:
-
✅
NanoTrace_Development.txt– a complete Bash-driven development toolkit with all core scripts (setup, backend/frontend build, Fabric network, QR integration, etc.) -
✅
NanoTrace_Full_Development_Plan.docx– a structured, strategic roadmap with architecture, modules, phases, monetization, and security.
✅ Step 1: Confirm Dev Environment is Ready
To begin building, we must confirm your terminal is ready to execute the NanoTrace system bootstrap.
Let’s verify your development system meets the requirements.
Please run the following in your terminal:
bash# STEP 1: Confirm you're in the home directory
cd ~
# STEP 2: Check if NanoTrace base folder exists
ls /home/michal/NanoTrace
🧭 Question for you:
-
Has
/home/michal/NanoTracealready been created by a previous script? -
Or do you want to run the full setup from scratch, including the chaincode, Flask backend, frontend, and PostgreSQL database?
Let me know your answer, and I’ll guide you with the exact next command.
When ready, we’ll move to:
🔜 Step 2: Run setup.sh to generate full project structure, .env, and Python venv
Waiting for your confirmation first.
You said:
ChatGPT said:
Perfect. You've uploaded Core Setup Scripts.txt, which aligns exactly with the full NanoTrace_Development.txt script set. That means you're holding a complete industrial-grade blockchain certification system builder — already modular, scripted, and ready to deploy.
Let’s now begin building step-by-step.
✅ STEP 1: Run the Initial Setup Script
This step will:
-
Create the
/home/michal/NanoTraceproject structure -
Set up Python virtual environment (
venv/) -
Generate
.envconfig with Flask, DB, Redis, Blockchain, QR settings -
Output next actions clearly
💻 Run this in your terminal:
bashcd /home/michal/NanoTrace || mkdir -p /home/michal/NanoTrace && cd /home/michal/NanoTrace
# Download or move setup.sh here if not already
nano setup.sh # Paste the content if not yet in place, or confirm it's already created
# Make it executable
chmod +x setup.sh
# 🚀 Run the script
./setup.sh
✅ Once you've run this, please tell me:
-
Was the
/home/michal/NanoTracestructure created successfully? -
Did it output:
✅ Project setup complete!? -
Are you now inside the activated Python environment (
(venv)at the terminal prompt)?
🔜 After you confirm, we’ll proceed to:
STEP 2: Install System Dependencies
(PostgreSQL, Docker, Go, Redis, NGINX, Node, Certbot, etc.)
This will prepare your system to run blockchain, frontend/backend, and deploy securely.
Awaiting your confirmation from step 1.
You said:
i am in /home/michal/NanoTrace/
ChatGPT said:
Great job — you're right where you need to be:
📁 /home/michal/NanoTrace/
✅ STEP 2: Install All System Dependencies
This script will install:
-
🐳 Docker + Docker Compose (for Fabric and deployment)
-
🐘 PostgreSQL (for user & cert database)
-
🧠 Redis (for async tasks, session cache, QR processing)
-
⚙️ Go (to compile Fabric chaincode)
-
⚛️ Node.js (for React frontend)
-
🌐 NGINX + Certbot (for production deployment & HTTPS)
💻 Run the following in your terminal:
bash# Make sure script is present
nano install_dependencies.sh # Confirm or paste script if not yet saved
# Make it executable
chmod +x install_dependencies.sh
# Run it
./install_dependencies.sh
⚠️ While it runs:
The script might prompt for:
-
sudopassword (for installing packages) -
Environment reload (
source ~/.bashrc) if Go or Docker was just installed
✅ After the script finishes, tell me:
-
Did it complete with
✅ System dependencies installed!? -
Do
docker,go, andnodeall work in your terminal now?
Try these:
bashdocker --version go version node -v
Once confirmed, we’ll move to:
🔜 STEP 3: PostgreSQL Database Setup
(We'll create the nanotrace DB + user + verify access)
Let me know when you're ready.
You said:
let create project structure
ChatGPT said:
Excellent — let’s create the full NanoTrace project structure now.
This is handled by setup.sh, which you've already staged.
✅ STEP 1A: Run the Setup Script
This will:
-
Create full directory tree:
backend/,frontend/,chaincode/, etc. -
Set up Python virtual environment
-
Create
.envconfiguration template -
Output clear next steps
💻 Run this in your terminal:
bashchmod +x setup.sh
./setup.sh
If you see:
🚀 Setting up NanoTrace development environment...
and it ends with:
✅ Project setup complete!
— then everything went well.
🧠 After the script runs, confirm:
-
✅ Did the directory tree get created under
/home/michal/NanoTrace/?
You should see:backend/ chaincode/ deployment/ docs/ frontend/ scripts/ tests/ venv/ -
✅ Are you now inside the Python virtual environment (
(venv)should appear in your prompt)?
If not, activate manually:bashsource venv/bin/activate
Let me know once this is done and I’ll guide you immediately to:
🔜 STEP 2: PostgreSQL Database Setup (with setup_database.sh)
You said:
#!/bin/bash
set -e
echo "🚀 Setting up NanoTrace development environment..."
# Create project directory structure
PROJECT_ROOT="/home/michal/NanoTrace"
mkdir -p $PROJECT_ROOT
cd $PROJECT_ROOT
this
echo "📁 Creating directory structure..."
mkdir -p {chaincode,backend,frontend,deployment,docs,tests,scripts}
mkdir -p backend/{app,config,migrations}
mkdir -p backend/app/{models,views,utils,templates,static}
mkdir -p frontend/{src,public,build}
mkdir -p deployment/{systemd,nginx,ssl,docker}
mkdir -p tests/{unit,integration}
mkdir -p scripts/{fabric,qr,utils}
ChatGPT said:
Perfect — you're looking at the core of setup.sh, and yes, this is exactly the script that creates the full NanoTrace project structure.
Let’s finish setting it up, make it executable, and run it.
✅ Finalize and Run setup.sh
1. 📄 Paste the full script into a file
If you've only pasted part of the script, open the editor and paste the full version (from NanoTrace_Development.txt or Core Setup Scripts):
bashnano setup.sh
Paste the full content of setup.sh, making sure it includes:
-
virtualenv setup
-
requirements.txt -
.envgeneration -
final
echomessages
Then save and exit.
2. 🔐 Make the script executable
bashchmod +x setup.sh
3. 🚀 Run the script
bash./setup.sh
You should see messages like:
-
🚀 Setting up NanoTrace development environment... -
📁 Creating directory structure... -
🐍 Setting up Python virtual environment... -
📦 Installing Python dependencies... -
🔧 Creating environment configuration... -
✅ Project setup complete!
📌 After Running — Please Confirm:
-
✅ Was the project structure created?
Run:bashtree -L 2 /home/michal/NanoTrace -
✅ Are you in the Python virtual environment?
Run:bashsource venv/bin/activate
Then we proceed to:
🔜 STEP 2: setup_database.sh – Set up PostgreSQL for NanoTrace
Let me know when your setup is complete and you’re ready.
You said:
michal@quantworld:~$ ./setup.sh
Setting up NanoTrace development environment...
📁 Creating directory structure...
michal@quantworld:~$
ChatGPT said:
Nice! You're rolling — but it looks like your script may have been cut off early or didn’t include the full logic (virtualenv, requirements, .env setup, etc.).
Your terminal output stopped at:
bash📁 Creating directory structure...
That means only the folders were created — we’re missing the rest of setup.sh.
✅ Let’s fix it now by adding the full content.
1. 💻 Open the setup.sh file again:
bashnano setup.sh
2. 🔁 Replace everything with this full version (core content from your files):
bash#!/bin/bash
set -e
echo "🚀 Setting up NanoTrace development environment..."
# Create project directory structure
PROJECT_ROOT="/home/michal/NanoTrace"
mkdir -p $PROJECT_ROOT
cd $PROJECT_ROOT
echo "📁 Creating directory structure..."
mkdir -p {chaincode,backend,frontend,deployment,docs,tests,scripts}
mkdir -p backend/{app,config,migrations}
mkdir -p backend/app/{models,views,utils,templates,static}
mkdir -p frontend/{src,public,build}
mkdir -p deployment/{systemd,nginx,ssl,docker}
mkdir -p tests/{unit,integration}
mkdir -p scripts/{fabric,qr,utils}
# Create Python virtual environment
echo "🐍 Setting up Python virtual environment..."
python3 -m venv venv
source venv/bin/activate
# Install Python dependencies
echo "📦 Installing Python dependencies..."
cat > requirements.txt << 'PYTHON_DEPS'
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-Login==0.6.3
Flask-WTF==1.1.1
Flask-Mail==0.9.1
Flask-Migrate==4.0.5
psycopg2-binary==2.9.7
bcrypt==4.0.1
qrcode[pil]==7.4.2
requests==2.31.0
python-dotenv==1.0.0
gunicorn==21.2.0
celery==5.3.2
redis==4.6.0
cryptography==41.0.4
Pillow==10.0.0
PYTHON_DEPS
pip install -r requirements.txt
# Create .env template
echo "🔧 Creating environment configuration..."
cat > .env << 'ENV_CONFIG'
# Flask Configuration
FLASK_APP=backend/app
FLASK_ENV=development
SECRET_KEY=your-secret-key-change-in-production
# Database Configuration
DATABASE_URL=postgresql://nanotrace:password@localhost/nanotrace
# Email Configuration
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
# Blockchain Configuration
FABRIC_CA_URL=https://localhost:7054
FABRIC_PEER_URL=grpc://localhost:7051
FABRIC_ORDERER_URL=grpc://localhost:7050
# Redis Configuration
REDIS_URL=redis://localhost:6379/0
# QR Code Configuration
QR_CODE_PATH=/home/michal/NanoTrace/backend/app/static/qr_codes
ENV_CONFIG
echo "✅ Project setup complete!"
echo "Next steps:"
echo " 1. Update .env with your actual values"
echo " 2. Run ./install_dependencies.sh"
echo " 3. Run ./setup_database.sh"
3. ✅ Save and exit the editor (Ctrl+O, Enter, then Ctrl+X)
4. 🚀 Run again:
bash./setup.sh
🎯 Once done, please confirm:
-
✅ Project setup complete!is displayed -
(venv)shows in your terminal prompt -
This works:
bashpip list
Then we move to:
🔜 STEP 2: Set up PostgreSQL with setup_database.sh
Let’s keep building!
You said:
michal@quantworld:~$ ./setup.sh
Setting up NanoTrace development environment...
📁 Creating directory structure...
michal@quantworld:~$ nano setup.sh
michal@quantworld:~$ ./setup.sh
Setting up NanoTrace development environment...
📁 Creating directory structure...
🐍 Setting up Python virtual environment...
The virtual environment was not created successfully because ensurepip is not
available. On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.
apt install python3.12-venv
You may need to use sudo with that command. After installing the python3-venv
package, recreate your virtual environment.
Failing command: /home/michal/NanoTrace/venv/bin/python3
michal@quantworld:~$
ChatGPT said:
Excellent — you're right on track. The error is simple and common:
⚠️ Your system is missing the
python3.12-venvpackage, which is required to create a virtual environment for Python 3.12.
✅ Fix It in One Step
💻 Run this:
bashsudo apt update sudo apt install -y python3.12-venv
This will install the missing component for virtual environment creation.
🔁 Then: Delete and recreate your venv
Since the previous attempt failed partially, let's clean and retry:
bash# Remove the broken venv
rm -rf /home/michal/NanoTrace/venv
# Rerun the setup
./setup.sh
You should now see:
-
🐍 Setting up Python virtual environment... -
📦 Installing Python dependencies... -
🔧 Creating environment configuration... -
✅ Project setup complete!
Please confirm once done:
-
[ ]Was the venv created and activated? -
[ ]Didpip installsucceed? -
[ ].envfile created?
Then we're ready for:
🔜 STEP 3: PostgreSQL Database Setup (setup_database.sh)
Let me know when you're ready.
You said:
Get:3 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 python3.12-venv amd64 3.12.3-1ubuntu0.8 [5678 B]
Fetched 2425 kB in 0s (30.9 MB/s)
Selecting previously unselected package python3-pip-whl.
(Reading database ... 106924 files and directories currently installed.)
Preparing to unpack .../python3-pip-whl_24.0+dfsg-1ubuntu1.2_all.deb ...
Unpacking python3-pip-whl (24.0+dfsg-1ubuntu1.2) ...
Selecting previously unselected package python3-setuptools-whl.
Preparing to unpack .../python3-setuptools-whl_68.1.2-2ubuntu1.2_all.deb ...
Unpacking python3-setuptools-whl (68.1.2-2ubuntu1.2) ...
Selecting previously unselected package python3.12-venv.
Preparing to unpack .../python3.12-venv_3.12.3-1ubuntu0.8_amd64.deb ...
Unpacking python3.12-venv (3.12.3-1ubuntu0.8) ...
Setting up python3-setuptools-whl (68.1.2-2ubuntu1.2) ...
Setting up python3-pip-whl (24.0+dfsg-1ubuntu1.2) ...
Setting up python3.12-venv (3.12.3-1ubuntu0.8) ...
Scanning processes...
Scanning candidates...
Scanning linux images...
Pending kernel upgrade!
Running kernel version:
6.8.0-63-generic
Diagnostics:
The currently running kernel version is not the expected kernel version 6.8.0-78-generic.
Restarting the system to load the new kernel will not be handled automatically, so you should consider rebooting.
Restarting services...
Service restarts being deferred:
/etc/needrestart/restart.d/dbus.service
systemctl restart serial-getty@ttyS0.service
systemctl restart systemd-logind.service
systemctl restart unattended-upgrades.service
No containers need to be restarted.
User sessions running outdated binaries:
root @ session #5: bash[1180], login[794], su[1433,3021]
root @ user manager service: systemd[1166]
No VM guests are running outdated hypervisor (qemu) binaries on this host.
michal@quantworld:~$
ChatGPT said:
Perfect — you’ve successfully installed the missing package:
✅ python3.12-venv
You're now ready to recreate the virtual environment and complete the setup.
✅ Finalize setup.sh – Clean and Re-run
Run this in your terminal:
bash# 1. Delete the failed venv (optional but cleaner)
rm -rf /home/michal/NanoTrace/venv
# 2. Re-run the full setup script
cd /home/michal/NanoTrace
./setup.sh
You should now see these in sequence:
-
🐍 Setting up Python virtual environment... -
📦 Installing Python dependencies... -
🔧 Creating environment configuration... -
✅ Project setup complete!
🧪 Verify After Setup:
Run:
bashsource venv/bin/activate
pip list
You should see packages like Flask, Flask-Mail, gunicorn, etc.
And confirm:
bashls .env
# Should show the .env file was created
Once confirmed, we move straight into:
🔜 STEP 3: PostgreSQL Setup (setup_database.sh)
Let me know when ready, and I’ll give you the exact next command.
You said:
michal@quantworld:~$ sudo apt update
sudo apt install -y python3.12-venv
[sudo] password for michal:
Get:1 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
Hit:2 http://archive.ubuntu.com/ubuntu noble InRelease
Get:3 http://archive.ubuntu.com/ubuntu noble-updates InRelease [126 kB]
Get:4 http://archive.ubuntu.com/ubuntu noble-backports InRelease [126 kB]
Get:5 http://security.ubuntu.com/ubuntu noble-security/main amd64 Packages [1083 kB]
Get:6 http://archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages [1353 kB]
Get:7 http://security.ubuntu.com/ubuntu noble-security/main amd64 Components [21.5 kB]
Get:8 http://security.ubuntu.com/ubuntu noble-security/universe amd64 Packages [881 kB]
Get:9 http://archive.ubuntu.com/ubuntu noble-updates/main amd64 Components [175 kB]
Get:10 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 Packages [1124 kB]
Get:11 http://security.ubuntu.com/ubuntu noble-security/universe amd64 Components [52.2 kB]
Get:12 http://security.ubuntu.com/ubuntu noble-security/restricted amd64 Components [212 B]
Get:13 http://security.ubuntu.com/ubuntu noble-security/multiverse amd64 Components [212 B]
Get:14 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 Components [377 kB]
Get:15 http://archive.ubuntu.com/ubuntu noble-updates/restricted amd64 Components [212 B]
Get:16 http://archive.ubuntu.com/ubuntu noble-updates/multiverse amd64 Components [940 B]
Get:17 http://archive.ubuntu.com/ubuntu noble-backports/main amd64 Components [7068 B]
Get:18 http://archive.ubuntu.com/ubuntu noble-backports/universe amd64 Components [19.2 kB]
Get:19 http://archive.ubuntu.com/ubuntu noble-backports/restricted amd64 Components [216 B]
Get:20 http://archive.ubuntu.com/ubuntu noble-backports/multiverse amd64 Components [212 B]
Fetched 5473 kB in 1s (3767 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
46 packages can be upgraded. Run 'apt list --upgradable' to see them.
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
python3-pip-whl python3-setuptools-whl
The following NEW packages will be installed:
python3-pip-whl python3-setuptools-whl python3.12-venv
0 upgraded, 3 newly installed, 0 to remove and 46 not upgraded.
Need to get 2425 kB of archives.
After this operation, 2771 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 python3-pip-whl all 24.0+dfsg-1ubuntu1.2 [1703 kB]
Get:2 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 python3-setuptools-whl all 68.1.2-2ubuntu1.2 [716 kB]
Get:3 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 python3.12-venv amd64 3.12.3-1ubuntu0.8 [5678 B]
Fetched 2425 kB in 0s (30.9 MB/s)
Selecting previously unselected package python3-pip-whl.
(Reading database ... 106924 files and directories currently installed.)
Preparing to unpack .../python3-pip-whl_24.0+dfsg-1ubuntu1.2_all.deb ...
Unpacking python3-pip-whl (24.0+dfsg-1ubuntu1.2) ...
Selecting previously unselected package python3-setuptools-whl.
Preparing to unpack .../python3-setuptools-whl_68.1.2-2ubuntu1.2_all.deb ...
Unpacking python3-setuptools-whl (68.1.2-2ubuntu1.2) ...
Selecting previously unselected package python3.12-venv.
Preparing to unpack .../python3.12-venv_3.12.3-1ubuntu0.8_amd64.deb ...
Unpacking python3.12-venv (3.12.3-1ubuntu0.8) ...
Setting up python3-setuptools-whl (68.1.2-2ubuntu1.2) ...
Setting up python3-pip-whl (24.0+dfsg-1ubuntu1.2) ...
Setting up python3.12-venv (3.12.3-1ubuntu0.8) ...
Scanning processes...
Scanning candidates...
Scanning linux images...
Pending kernel upgrade!
Running kernel version:
6.8.0-63-generic
Diagnostics:
The currently running kernel version is not the expected kernel version 6.8.0-78-generic.
Restarting the system to load the new kernel will not be handled automatically, so you should consider rebooting.
Restarting services...
Service restarts being deferred:
/etc/needrestart/restart.d/dbus.service
systemctl restart serial-getty@ttyS0.service
systemctl restart systemd-logind.service
systemctl restart unattended-upgrades.service
No containers need to be restarted.
User sessions running outdated binaries:
root @ session #5: bash[1180], login[794], su[1433,3021]
root @ user manager service: systemd[1166]
No VM guests are running outdated hypervisor (qemu) binaries on this host.
michal@quantworld:~$ # 1. Delete the failed venv (optional but cleaner)
rm -rf /home/michal/NanoTrace/venv
# 2. Re-run the full setup script
cd /home/michal/NanoTrace
./setup.sh
-bash: ./setup.sh: No such file or directory
michal@quantworld:~/NanoTrace$
ChatGPT said:
✅ Excellent work — you successfully installed python3.12-venv, and now we’re ready to rebuild cleanly.
🔧 Fix: You're already in /home/michal/NanoTrace, but the setup.sh file is missing or in the wrong place.
Let’s quickly restore and run it.
✅ Step-by-step fix:
1. Recreate the setup.sh script
Run:
bashnano setup.sh
Paste the full setup script (you can use the version I gave earlier — includes directory creation, venv, requirements.txt, and .env setup).
Then save with:
-
Ctrl + O, thenEnterto save -
Ctrl + Xto exit
2. Make it executable and run it
bashchmod +x setup.sh
./setup.sh
You should now see:
text🐍 Setting up Python virtual environment... 📦 Installing Python dependencies... 🔧 Creating environment configuration... ✅ Project setup complete!
✅ After setup, test that it worked:
bash# Activate virtualenv
source venv/bin/activate
# Check packages
pip list
# Check for .env
ls .env
Once confirmed, we’ll jump into:
🔜 STEP 3: PostgreSQL Database Setup (setup_database.sh)
Let me know when you're done and I’ll give you the next exact command.
You said:
Installing build dependencies ... done
Getting requirements to build wheel ... error
error: subprocess-exited-with-error
× Getting requirements to build wheel did not run successfully.
│ exit code: 1
╰─> [34 lines of output]
/tmp/pip-build-env-luv9s226/overlay/lib/python3.12/site-packages/setuptools/dist.py:759: SetuptoolsDeprecationWarning: License classifiers are deprecated.
!!
********************************************************************************
Please consider removing the following classifiers in favor of a SPDX license expression:
License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details.
********************************************************************************
!!
self._finalize_license_expression()
running egg_info
writing psycopg2_binary.egg-info/PKG-INFO
writing dependency_links to psycopg2_binary.egg-info/dependency_links.txt
writing top-level names to psycopg2_binary.egg-info/top_level.txt
Error: pg_config executable not found.
pg_config is required to build psycopg2 from source. Please add the directory
containing pg_config to the $PATH or specify the full executable path with the
option:
python setup.py build_ext --pg-config /path/to/pg_config build ...
or with the pg_config option in 'setup.cfg'.
If you prefer to avoid building psycopg2 from source, please install the PyPI
'psycopg2-binary' package instead.
For further information please check the 'doc/src/install.rst' file (also at
<https://www.psycopg.org/docs/install.html>).
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error
× Getting requirements to build wheel did not run successfully.
│ exit code: 1
╰─> See above for output.
note: This error originates from a subprocess, and is likely not a problem with pip.
michal@quantworld:~/NanoTrace$
ChatGPT said:
You're doing great, Michal — and this is a very common and easily fixable issue when working with Python and PostgreSQL.
⚠️ Problem Summary:
The error is:
Error: pg_config executable not found.
This means you're missing the system-level PostgreSQL development headers needed to compile psycopg2.
✅ Fix in One Command:
bashsudo apt install -y libpq-dev
This package includes the missing pg_config binary.
🔁 Then re-run setup.sh
Since setup.sh failed during the pip install part, simply re-run:
bash./setup.sh
You should now get:
-
📦 Installing Python dependencies...✅ success -
🔧 Creating environment configuration... -
✅ Project setup complete!
🧪 Confirm after running:
bashsource venv/bin/activate
pip show psycopg2-binary
You should see its version (e.g., 2.9.7).
✅ Once confirmed, we go straight to:
🔜 STEP 3: PostgreSQL DB Setup with setup_database.sh
Let me know when you're ready.
You said:
Collecting gunicorn==21.2.0 (from -r requirements.txt (line 12))
Downloading gunicorn-21.2.0-py3-none-any.whl.metadata (4.1 kB)
ERROR: Ignored the following yanked versions: 5.0.6, 5.2.5
ERROR: Could not find a version that satisfies the requirement celery==5.3.2 (from versions: 0.1.2, 0.1.4, 0.1.6, 0.1.7, 0.1.8, 0.1.10, 0.1.11, 0.1.12, 0.1.13, 0.1.14, 0.1.15, 0.2.0, 0.3.0, 0.3.7, 0.3.20, 0.4.0, 0.4.1, 0.6.0, 0.8.0, 0.8.1, 0.8.2, 0.8.3, 0.8.4, 1.0.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4, 1.0.5, 1.0.6, 2.0.0, 2.0.1, 2.0.2, 2.0.3, 2.1.0, 2.1.1, 2.1.2, 2.1.3, 2.1.4, 2.2.0, 2.2.1, 2.2.2, 2.2.3, 2.2.4, 2.2.5, 2.2.6, 2.2.7, 2.2.8, 2.2.9, 2.2.10, 2.3.0, 2.3.1, 2.3.2, 2.3.3, 2.3.4, 2.3.5, 2.4.0, 2.4.1, 2.4.2, 2.4.3, 2.4.4, 2.4.5, 2.4.6, 2.4.7, 2.5.0, 2.5.1, 2.5.2, 2.5.3, 2.5.5, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, 3.0.5, 3.0.6, 3.0.7, 3.0.8, 3.0.9, 3.0.10, 3.0.11, 3.0.12, 3.0.13, 3.0.14, 3.0.15, 3.0.16, 3.0.17, 3.0.18, 3.0.19, 3.0.20, 3.0.21, 3.0.22, 3.0.23, 3.0.24, 3.0.25, 3.1.0, 3.1.1, 3.1.2, 3.1.3, 3.1.4, 3.1.5, 3.1.6, 3.1.7, 3.1.8, 3.1.9, 3.1.10, 3.1.11, 3.1.12, 3.1.13, 3.1.14, 3.1.15, 3.1.16, 3.1.17, 3.1.18, 3.1.19, 3.1.20, 3.1.21, 3.1.22, 3.1.23, 3.1.24, 3.1.25, 3.1.26.post1, 3.1.26.post2, 4.0.0rc3, 4.0.0rc4, 4.0.0rc5, 4.0.0rc6, 4.0.0rc7, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.1.1, 4.2.0rc1, 4.2.0rc2, 4.2.0rc3, 4.2.0rc4, 4.2.0, 4.2.1, 4.2.2, 4.3.0rc1, 4.3.0rc2, 4.3.0rc3, 4.3.0, 4.3.1, 4.4.0rc1, 4.4.0rc2, 4.4.0rc3, 4.4.0rc4, 4.4.0rc5, 4.4.0, 4.4.1, 4.4.2, 4.4.3, 4.4.4, 4.4.5, 4.4.6, 4.4.7, 5.0.0a1, 5.0.0a2, 5.0.0b1, 5.0.0rc1, 5.0.0rc2, 5.0.0rc3, 5.0.0, 5.0.1, 5.0.2, 5.0.3, 5.0.4, 5.0.5, 5.1.0b1, 5.1.0b2, 5.1.0rc1, 5.1.0, 5.1.1, 5.1.2, 5.2.0b1, 5.2.0b2, 5.2.0b3, 5.2.0rc1, 5.2.0rc2, 5.2.0, 5.2.1, 5.2.2, 5.2.3, 5.2.4, 5.2.6, 5.2.7, 5.3.0a1, 5.3.0b1, 5.3.0b2, 5.3.0rc1, 5.3.0rc2, 5.3.0, 5.3.1, 5.3.4, 5.3.5, 5.3.6, 5.4.0rc1, 5.4.0rc2, 5.4.0, 5.5.0b1, 5.5.0b2, 5.5.0b3, 5.5.0b4, 5.5.0rc1, 5.5.0rc2, 5.5.0rc3, 5.5.0rc4, 5.5.0rc5, 5.5.0, 5.5.1, 5.5.2, 5.5.3)
ERROR: No matching distribution found for celery==5.3.2
michal@quantworld:~/NanoTrace$
ChatGPT said:
You're progressing well — this error is simply due to celery==5.3.2 being yanked from PyPI.
✅ Solution: Use a stable version of Celery that is still published
Let’s pin a working version — celery==5.3.1 is safe and compatible with your stack.
🔁 Step-by-step Fix
1. 📝 Edit requirements.txt to change only the Celery version:
bashnano requirements.txt
Find this line:
txtcelery==5.3.2
And replace it with:
txtcelery==5.3.1
Then save and exit:
-
Ctrl + O→ Enter -
Ctrl + X
2. 📦 Re-run the dependency install:
If you're in the venv, just run:
bashpip install -r requirements.txt
If not, activate first:
bashsource venv/bin/activate
pip install -r requirements.txt
✅ Once you see Successfully installed celery-5.3.1, confirm with:
bashpip show celery
Let me know once that's done — then we’ll proceed with:
🔜 STEP 3: PostgreSQL setup via setup_database.sh (create DB, test connection)
You said:
ichal@quantworld:~/NanoTrace$ nano requirements.txt
michal@quantworld:~/NanoTrace$ pip install -r requirements.txt
Command 'pip' not found, but can be installed with:
sudo apt install python3-pip
michal@quantworld:~/NanoTrace$
ChatGPT said:
✅ Good catch — that just means pip isn’t installed yet on your Ubuntu system.
Let’s fix that in one step.
✅ Install pip for Python 3
Run:
bashsudo apt install -y python3-pip
After that, ensure you're inside your virtual environment:
bashsource venv/bin/activate
You’ll know it’s active if your terminal prompt starts with (venv).
Then install your dependencies:
bashpip install -r requirements.txt
Once you get Successfully installed ..., confirm:
bashpip list | grep celery
Should show:
nginxcelery 5.3.1
After that, we’re finally ready for:
🔜 STEP 3: PostgreSQL Setup (setup_database.sh)
Let me know once pip install works and you're ready.
You said:
ready
ChatGPT said:
Perfect — we now enter:
✅ STEP 3: PostgreSQL Database Setup
This step will:
-
Start the PostgreSQL service
-
Create:
-
nanotracePostgreSQL user -
nanotracedatabase
-
-
Grant all privileges to the user
-
Test DB connection via
psql
💻 1. Confirm or Create setup_database.sh
Run:
bashnano setup_database.sh
Paste the full script:
bash#!/bin/bash
set -e
echo "🐘 Setting up PostgreSQL database..."
# Start PostgreSQL service
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Create database user and database
sudo -u postgres psql << 'PSQL_COMMANDS'
CREATE USER nanotrace WITH PASSWORD 'password';
CREATE DATABASE nanotrace OWNER nanotrace;
GRANT ALL PRIVILEGES ON DATABASE nanotrace TO nanotrace;
\q
PSQL_COMMANDS
# Test connection
echo "🔍 Testing database connection..."
PGPASSWORD=password psql -h localhost -U nanotrace -d nanotrace -c "SELECT version();"
echo "✅ Database setup complete!"
Save with Ctrl + O, Enter, then Ctrl + X
🔐 2. Make it executable and run:
bashchmod +x setup_database.sh
./setup_database.sh
You should see:
-
🐘 Setting up PostgreSQL database... -
🔍 Testing database connection... -
✅ Database setup complete!
🧠 After Running — Confirm:
bash# Check DB exists
sudo -u postgres psql -c "\l" | grep nanotrace
Let me know if you see the success message — and we’ll move to:
🔜 STEP 4: Hyperledger Fabric Setup (setup_fabric.sh)
(Will create your Fabric Docker network and base config.)
You said:
Setting up PostgreSQL database...
Failed to start postgresql.service: Unit postgresql.service not found.
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfectly clear — this just means PostgreSQL is not yet installed on your system.
Let’s install it now and rerun the script.
✅ Step 1: Install PostgreSQL + Required Tools
Run:
bashsudo apt update sudo apt install -y postgresql postgresql-contrib libpq-dev
This will install:
-
postgresql(the database server) -
libpq-dev(needed forpsycopg2) -
postgresql-contrib(extra tools)
🔁 Step 2: Rerun your database setup script
bash./setup_database.sh
It should now:
-
Start PostgreSQL service
-
Create the user & database
-
Test connection
-
✅ Confirm with:
Database setup complete!
Let me know when that’s done — and we’ll continue to:
🔜 STEP 4: Hyperledger Fabric Setup (setup_fabric.sh)
You said:
in venv
ChatGPT said:
Perfect — being in the (venv) is exactly what we want.
Now let's install PostgreSQL on the system (PostgreSQL runs outside the virtual environment), then run your script again.
✅ Run these commands in your terminal, not inside Python:
bash# Make sure you're in your project root
cd ~/NanoTrace
# Exit virtual environment just for clarity (optional)
deactivate
# Install PostgreSQL + dependencies
sudo apt update
sudo apt install -y postgresql postgresql-contrib libpq-dev
This will install the system-level PostgreSQL service + development headers.
✅ Then re-enter your virtual environment:
bashcd ~/NanoTrace
source venv/bin/activate
Now re-run the database setup:
bash./setup_database.sh
🧠 You should see:
-
🐘 Setting up PostgreSQL database... -
🔍 Testing database connection... -
✅ Database setup complete!
Let me know when done — and then we move forward to:
🔜 STEP 4: setup_fabric.sh (Hyperledger Fabric + Docker Network Setup)
You said:
so exit venv?
ChatGPT said:
Yes — just temporarily, to install PostgreSQL system-wide. Here's the exact path forward:
✅ Step-by-step:
1. 🚪 Exit the virtual environment
bashdeactivate
You should no longer see (venv) in your terminal prompt.
2. 🧱 Install PostgreSQL and required development libraries
bashsudo apt update sudo apt install -y postgresql postgresql-contrib libpq-dev
This will:
-
Start the PostgreSQL service
-
Add the
postgresuser and database tools -
Ensure
pg_configworks forpsycopg2
3. 🔁 Re-enter your virtual environment
bashcd ~/NanoTrace
source venv/bin/activate
You’ll see (venv) again.
4. 🚀 Now re-run your DB setup script
bash./setup_database.sh
✅ Confirm when you see:
text🔍 Testing database connection... ✅ Database setup complete!
Then we proceed to:
🔜 STEP 4: Blockchain network setup with setup_fabric.sh (Fabric Docker config, start script)
You said:
version
--------------------------------------------------------------------------------------------------------------------------------------
PostgreSQL 16.9 (Ubuntu 16.9-0ubuntu0.24.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
(1 row)
(END)
ChatGPT said:
✅ Boom! That output confirms:
PostgreSQL is installed, running, and the
nanotraceDB is accessible.
Your database setup is complete.
🎯 Recap:
-
🗂️ Project directory:
/home/michal/NanoTrace -
🐍 Python venv: activated and all dependencies installed
-
🐘 PostgreSQL: running and configured for
nanotrace -
✅
.envfile created and ready
🔜 STEP 4: Blockchain Setup with setup_fabric.sh
This step will:
-
Download Hyperledger Fabric 2.5.4 binaries & samples
-
Create basic Docker Compose network
-
Scaffold org folders, configtx, and peer/orderer containers
-
Prepare the start script:
start_network.sh
✅ Let’s do it:
1. Check or create setup_fabric.sh:
bashnano setup_fabric.sh
Paste the full script if needed (you can copy from your NanoTrace_Development.txt).
Then save and:
bashchmod +x setup_fabric.sh
./setup_fabric.sh
⏳ This step may take a few minutes — it downloads binaries and builds the network.
Once done, you’ll see:
-
✅ Fabric configuration created!
-
Run
./fabric-network/network/scripts/start_network.shto start the network
Let me know when you're done — and I’ll guide you into:
🔜 STEP 5: Flask Backend Setup (build_backend.sh)
(You’ll wire up routes, models, QR logic, and the entire app.)
You said:
nano setup_fabric.sh in venv?
ChatGPT said:
✅ Yes — it's totally fine to run nano setup_fabric.sh inside the venv, since you're just editing a shell script (not running Python code yet).
But when executing Fabric commands like curl, docker, etc., the venv doesn't matter.
🧠 Summary:
| Action | OK inside venv? |
|---|---|
nano setup_fabric.sh | ✅ Yes |
chmod +x setup_fabric.sh | ✅ Yes |
./setup_fabric.sh | ✅ Yes |
docker-compose, bash, etc. | ✅ Yes (venv doesn’t affect system binaries like Docker) |
✅ Next Steps
-
Run:
bashnano setup_fabric.sh
-
Paste the full script (from your
NanoTrace_Development.txtor I can give it again if needed). -
Then:
bashchmod +x setup_fabric.sh
./setup_fabric.sh
Let it finish downloading Fabric and creating your network config.
Once done, tell me:
-
✅ Did it say:
✅ Fabric network configuration created!? -
✅ Was
fabric-network/folder created?
Then we’ll proceed with:
🔜 STEP 5: Build the Flask Backend (build_backend.sh)
You said:
how to exit db
ChatGPT said:
To exit the PostgreSQL interactive shell (psql), simply type:
sql\q
Then press Enter.
You’ll return to your normal terminal prompt (e.g., (venv) michal@quantworld:~$).
Let me know once you're out of the database and ready to continue with:
🔜 STEP 4: setup_fabric.sh — Fabric blockchain setup
You said:
do we have 2. install_dependencies.sh - Install system dependencies
cat > install_dependencies.sh << 'EOF'
#!/bin/bash
set -e
echo "🔧 Installing system dependencies..."
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install essential packages
sudo apt install -y \
curl \
wget \
git \
build-essential \
python3 \
python3-venv \
python3-pip \
postgresql \
postgresql-contrib \
redis-server \
nginx \
certbot \
python3-certbot-nginx
ChatGPT said:
Yes 👍 — that’s exactly the install_dependencies.sh script from your NanoTrace_Development.txt and Core Setup Scripts.txt.
That script is responsible for installing all the system-level tools your project needs.
✅ What it Installs
-
Core build tools:
curl,wget,git,build-essential -
Python 3 + venv + pip
-
PostgreSQL + contrib tools
-
Redis server
-
NGINX (webserver / reverse proxy)
-
Certbot (for SSL certificates via Let’s Encrypt)
And further down in the script (after your snippet), it also installs:
-
Docker + Docker Compose (if missing)
-
Go (for Hyperledger Fabric chaincode)
-
Node.js + npm (for the React frontend)
📌 Next Steps
Since you already installed PostgreSQL manually, running this script now will:
-
Ensure you have Redis, Nginx, Certbot, Go, Node.js, Docker installed
-
Update any missing dependencies in one go
💻 Run it now:
bashchmod +x install_dependencies.sh
./install_dependencies.sh
🔎 When finished, verify with:
bashdocker --version go version node -v npm -v redis-server --version nginx -v
Do you want me to paste the full install_dependencies.sh script here so you can compare with what you already have, or are you ready to run it directly?
You said:
# Install essential packages
sudo apt install -y \
curl \
wget \
git \
build-essential \
python3 \
python3-venv \
python3-pip \
postgresql \
postgresql-contrib \
redis-server \
nginx \
certbot \
python3-certbot-nginx
# Install Docker
echo "🐳 Installing Docker..."
if ! command -v docker &> /dev/null; then
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
fi
# Install Docker Compose
if ! command -v docker-compose &> /dev/null; then
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
fi
# Install Go (for chaincode development)
echo "🔧 Installing Go..."
if ! command -v go &> /dev/null; then
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
export PATH=$PATH:/usr/local/go/bin
fi
# Install Node.js and npm (for frontend development)
echo "📦 Installing Node.js..."
if ! command -v node &> /dev/null; then
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
fi
echo "✅ System dependencies installed!"
echo "Please run 'source ~/.bashrc' or restart your terminal to update PATH"
EOF
ChatGPT said:
Perfect ✅ — that’s the full install_dependencies.sh script exactly as it should be.
It covers everything: system tools, PostgreSQL, Redis, NGINX, Docker + Compose, Go, and Node.js.
📌 Next Steps
-
Save it into your project root (if not already):
bashnano install_dependencies.sh
Paste the full content you showed, then save (Ctrl+O, Enter, Ctrl+X).
-
Make it executable:
bashchmod +x install_dependencies.sh
-
Run it 🚀:
bash./install_dependencies.sh
This will take some time because it may:
-
Install/update system packages
-
Pull Docker installer
-
Extract Go binaries
-
Install Node.js
🔎 After it finishes, verify installs:
bashdocker --version docker-compose --version go version node -v npm -v redis-server --version nginx -v
You should see version outputs for each.
👉 Do you want me to give you a compact checklist of expected versions (so you can compare after running), or should we just run it and troubleshoot if something is missing?
You said:
what i need Git for
ChatGPT said:
Great question 👌
Here’s why Git is in your dependencies list:
🔧 Why Git is Needed in NanoTrace
-
Cloning external repos
-
Hyperledger Fabric samples and binaries are often pulled from GitHub.
-
Example: Fabric’s
test-networkconfigs or chaincode templates.
-
-
Version control (optional but useful)
-
Even though you said you don’t need a Git repo for personal dev,
Git lets you snapshot your code, rollback, or branch off if needed.
-
-
Package installation
-
Some Python/Node dependencies are only available via GitHub links.
-
pipornpmmay invoke Git to fetch them.
-
✅ In Your Case
-
You don’t need Git for daily coding if you’re building solo.
-
But you do need Git installed on the system so the setup scripts (Fabric, Go, Node, etc.) don’t break when they try to pull from GitHub.
⚡ Short answer:
You need Git installed, even if you don’t plan to use it directly.
It’s mainly for Fabric, dependencies, and installer scripts.
👉 Do you want me to trim install_dependencies.sh
to only include the packages absolutely required for NanoTrace
(dropping optional tools), or keep it full for maximum flexibility?
You said:
Package configuration
┌─────────────────────────────────────────┤ Configuring openssh-server ├─────────────────────────────────────────┐
│ A new version (/tmp/tmp.u4tyZfHOrM) of configuration file /etc/ssh/sshd_config is available, but the version │
│ installed currently has been locally modified. │
│ │
│ What do you want to do about modified configuration file sshd_config? │
│ │
│ install the package maintainer's version │
│ keep the local version currently installed │
│ show the differences between the versions │
│ show a side-by-side difference between the versions │
│ show a 3-way difference between available versions │
│ do a 3-way merge between available versions │
│ start a new shell to examine the situation │
│
ChatGPT said:
Great catch. Because you’re on a remote box and likely customized SSH (non-default port, hardening, Fail2Ban, etc.), the safest business-grade choice right now is:
Step 1 — In the dialog
Choose: “keep the local version currently installed.”
Reason: this avoids accidentally overwriting your hardened sshd_config and locking yourself out of the server (expensive downtime risk).
Step 2 — After apt finishes, review and optionally merge upstream changes (from terminal)
Run these in your terminal:
bash# 1) Back up your current config
sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.$(date +%F)
# 2) Check if a .dpkg-dist (or similar) was dropped
ls -1 /etc/ssh/sshd_config* | sed -n '1,5p'
# Common names: sshd_config.dpkg-dist or sshd_config.ucf-dist
# 3) If it exists, compare side by side
sudo diff -u /etc/ssh/sshd_config /etc/ssh/sshd_config.dpkg-dist || true
If you want to merge a few upstream defaults (e.g., updated ciphers/KEX/mac lists), open both and edit:
bashsudo nano /etc/ssh/sshd_config
# (open a second terminal to view the -dist file)
sudo nano /etc/ssh/sshd_config.dpkg-dist
When finished, validate before touching the service:
bashsudo sshd -t
If there’s no output, the config is valid. Only then consider reloading:
bash# Only if you have a second SSH session open, so you can recover if needed:
sudo systemctl reload ssh
That’s it.
When you’re ready, we’ll continue with the Fabric setup script next.
You said:
Pending kernel upgrade!
Running kernel version:
6.8.0-63-generic
Diagnostics:
The currently running kernel version is not the expected kernel version 6.8.0-78-generic.
Restarting the system to load the new kernel will not be handled automatically, so you should consider rebooting.
Restarting services...
Service restarts being deferred:
/etc/needrestart/restart.d/dbus.service
systemctl restart serial-getty@ttyS0.service
systemctl restart systemd-logind.service
systemctl restart unattended-upgrades.service
No containers need to be restarted.
User sessions running outdated binaries:
michal @ session #8897: sshd[476342]
michal @ session #8901: sshd[476451,476508]
michal @ user manager service: systemd[476346]
root @ session #5: bash[1180], login[794], su[1433,3021]
root @ user manager service: systemd[1166]
No VM guests are running outdated hypervisor (qemu) binaries on this host.
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
That message is just Ubuntu telling you that a new kernel has been installed (6.8.0-78), but you’re still running the older one (6.8.0-63).
🧠 What it means:
-
All your updates & dependencies installed correctly. ✅
-
To actually use the new kernel (and apply security fixes), you need to reboot the server.
-
Until then, services will keep running fine on the current kernel.
🔐 Best practice for you (since this is a remote VPS):
-
Make sure you have a backup / second SSH session open before rebooting.
-
Run:
bashsudo reboot
-
After the system restarts, reconnect:
bashssh michal@217.154.34.203
-
Verify the kernel version:
bashuname -r
It should now show 6.8.0-78-generic.
👉 You don’t have to reboot immediately — you can finish setting up Fabric and backend, then reboot during a safe maintenance window.
⚡ Do you want to continue setting up Hyperledger Fabric (setup_fabric.sh) before reboot, or do a clean reboot now and then resume?
You said:
michal@michal-pc:~$ ssh michal@217.154.34.203
ssh: connect to host 217.154.34.203 port 22: Connection refused
michal@michal-pc:~$
ChatGPT said:
Got you, Michal. “Connection refused” means nothing’s listening on port 22 (or a firewall is blocking it). Let’s do this cleanly—two steps.
Step 1 — From your LOCAL terminal
Check which SSH port (if any) is open and try it.
bash# Quick probes (adjust IP if needed)
nc -vz 217.154.34.203 22 2288 2222 2200 # common custom ports
# If you have nmap:
nmap -Pn -p 1-1024,2200-2290 217.154.34.203 | grep -E 'open|ssh'
# Try a known custom port if you used one before (e.g., 2288):
ssh -p 2288 michal@217.154.34.203
If you get “succeeded”/“open” on a port, use that port with ssh -p.
Step 2 — If no port is open, use your provider’s SERVER console (out-of-band)
Log in via the VPS web console and run:
bash# 1) Is SSH running?
sudo systemctl status ssh || sudo systemctl status sshd
# 2) What port is configured?
sudo grep -nE '^[[:space:]]*Port[[:space:]]+' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null || echo "No Port line (defaults to 22)"
# 3) What is actually listening?
sudo ss -tlnp | grep -E '(:22|sshd)'
# 4) Firewall rules (UFW):
sudo ufw status verbose
Fixes (still on the SERVER console):
bash# If SSH is stopped:
sudo systemctl enable --now ssh
# If you changed to a custom port (example: 2288), open it in UFW:
sudo ufw allow 2288/tcp
sudo ufw reload
# If you reverted to 22, open 22:
sudo ufw allow 22/tcp
sudo ufw reload
# Validate config before restart:
sudo sshd -t && sudo systemctl restart ssh
Ping me with the outputs of Step 1 (or any error lines from Step 2), and I’ll steer the next move. You’re close.
You said:
i am log back
ChatGPT said:
Great—welcome back in, Michal. We’ll keep momentum and move one clean step at a time.
Step 1 (VPS terminal — quantworld): Verify toolchain is ready
Run these to confirm Docker/Go/Node are usable after your updates/reboot:
bashdocker --version docker ps go version node -v npm -v
-
If
docker pssays “permission denied”, fix it without reboot:bashsudo usermod -aG docker "$USER" newgrp docker docker ps
Step 2 (VPS terminal — quantworld): Fabric setup script
Create + run the Fabric setup (if you haven’t already saved it):
bashcd ~/NanoTrace
nano setup_fabric.sh
Paste the full content from your script set (the one that:
-
downloads Fabric binaries/samples for 2.5.4,
-
writes
network/docker/docker-compose-test-net.yaml, -
creates
network/scripts/start_network.sh).
Save, then:
bashchmod +x setup_fabric.sh
./setup_fabric.sh
You should see a success message like “✅ Fabric network configuration created!” and a fabric-network/ (or similar) directory with network/ inside.
Reply with:
-
the outputs from Step 1 (versions), and
-
the final lines from Step 2,
and I’ll guide the next move (starting the Fabric network and wiring the backend to it).
You said:
michal@michal-pc:~$ ssh michal@217.154.34.203
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-78-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Tue Aug 26 20:06:29 UTC 2025
System load: 0.02 Processes: 183
Usage of /: 6.5% of 463.92GB Users logged in: 0
Memory usage: 1% IPv4 address for ens6: 217.154.34.203
Swap usage: 0%
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
Last login: Tue Aug 26 18:09:56 2025 from 193.203.138.114
michal@quantworld:~$ docker --version
docker ps
go version
node -v
npm -v
Command 'docker' not found, but can be installed with:
sudo snap install docker # version 28.1.1+1, or
sudo apt install docker.io # version 26.1.3-0ubuntu1~24.04.1
sudo apt install podman-docker # version 4.9.3+ds1-1ubuntu0.2
See 'snap info docker' for additional versions.
Command 'docker' not found, but can be installed with:
sudo snap install docker # version 28.1.1+1, or
sudo apt install docker.io # version 26.1.3-0ubuntu1~24.04.1
sudo apt install podman-docker # version 4.9.3+ds1-1ubuntu0.2
See 'snap info docker' for additional versions.
Command 'go' not found, but can be installed with:
sudo snap install go # version 1.24.6, or
sudo apt install golang-go # version 2:1.21~2
sudo apt install gccgo-go # version 2:1.21~2
See 'snap info go' for additional versions.
Command 'node' not found, but can be installed with:
sudo apt install nodejs
Command 'npm' not found, but can be installed with:
sudo apt install npm
michal@quantworld:~$
ChatGPT said:
Nice clean reboot, Michal. You’re on kernel 6.8.0-78 now.
Your box is missing Docker, Go, Node — let’s handle that in two tight steps.
Step 1 — Install the system toolchain (run on VPS terminal)
bashcd ~/NanoTrace
# if the script isn’t there yet, create it from your notes, then:
chmod +x install_dependencies.sh
./install_dependencies.sh
This installs: Docker + Compose, Go, Node.js, Redis, NGINX, Certbot (and leaves PostgreSQL as-is since you already set it up).
Step 2 — Verify the toolchain
bashdocker --version && docker ps go version node -v && npm -v
If docker ps complains about permissions:
bashsudo usermod -aG docker "$USER"
newgrp docker
docker ps
Report back the version outputs. Once these are green, we’ll start Fabric (setup_fabric.sh) next.
You said:
# Install Docker
echo "🐳 Installing Docker..."
if ! command -v docker &> /dev/null; then
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
fi
# Install Docker Compose
if ! command -v docker-compose &> /dev/null; then
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
fi
# Install Go (for chaincode development)
echo "🔧 Installing Go..."
if ! command -v go &> /dev/null; then
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
export PATH=$PATH:/usr/local/go/bin
fi
# Install Node.js and npm (for frontend development)
echo "📦 Installing Node.js..."
if ! command -v node &> /dev/null; then
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
fi
echo "✅ System dependencies installed!"
echo "Please run 'source ~/.bashrc' or restart your terminal to update PATH"
EOF
ChatGPT said:
Great—this is the right block from install_dependencies.sh.
Step 1 (VPS terminal: quantworld)
Save and run the installer:
bashcd ~/NanoTrace
nano install_dependencies.sh # paste the whole script (including the lines you showed)
chmod +x install_dependencies.sh
./install_dependencies.sh
Step 2 (same terminal)
Refresh your Docker group and verify tools:
bashnewgrp docker docker --version && docker ps go version node -v && npm -v
Tell me the outputs. If anything fails, paste the error and I’ll fix it on the spot.
You said:
in venv
ChatGPT said:
Good you noticed that 👍
Here’s the key: system-level dependencies (Docker, Go, Node.js, NGINX, Redis, etc.) are not Python packages — so it doesn’t matter if you’re in the venv or not.
-
The
install_dependencies.shscript installs withsudo apt …,curl …, etc. -
That bypasses the venv completely and affects the whole server.
✅ What to do now
You can stay in the venv or deactivate — both work.
For clarity, I’d suggest deactivating while running system install scripts:
bashdeactivate
Then:
bashcd ~/NanoTrace
chmod +x install_dependencies.sh
./install_dependencies.sh
When it finishes, reload your environment so Go is in your PATH:
bashsource ~/.bashrc
🔎 Verify installs
bashdocker --version && docker ps go version node -v && npm -v
👉 Want me to give you the minimal test commands for Fabric right after this (to confirm Docker + Go can actually run Fabric containers), or shall we just move straight into setup_fabric.sh?
You said:
Scanning linux images...
Running kernel seems to be up-to-date.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host.
System dependencies installed!
Please run 'source ~/.bashrc' or restart your terminal to update PATH
./install_dependencies.sh: line 57: EOF: command not found
michal@quantworld:~/NanoTrace$
ChatGPT said:
Nice—most installs finished, but your script printed:
bash./install_dependencies.sh: line 57: EOF: command not found
That means there’s a stray literal EOF at the end of the file. Quick tidy + verify:
Step 1 — Fix the script tail (terminal)
bashcd ~/NanoTrace
nano install_dependencies.sh
-
Scroll to the very end and delete the lone
EOFline. -
Ensure the last lines are exactly:
bashecho "✅ System dependencies installed!"
echo "Please run 'source ~/.bashrc' or restart your terminal to update PATH"
Save (Ctrl+O, Enter) and exit (Ctrl+X).
Now reload your PATH additions and Docker group:
bashsource ~/.bashrc
newgrp docker
Step 2 — Verify the toolchain (terminal)
bashdocker --version && docker ps docker-compose --version go version node -v && npm -v redis-server --version nginx -v
If docker ps complains about permissions, run:
bashsudo usermod -aG docker "$USER"
newgrp docker
docker ps
Reply with those version outputs. Once we see they’re green, we’ll start the Fabric setup (setup_fabric.sh).
You said:
echo "Installing Node.js..."
if ! command -v node &> /dev/null; then
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
fi
echo "System dependencies installed!"
echo "Please run 'source ~/.bashrc' or restart your terminal to update PATH"
EOF
it was like that
ChatGPT said:
Got it—your script ended with a stray EOF because that block wasn’t opened with a cat << 'EOF' here-doc. Easy fix.
Step 1 — Fix the file tail (VPS terminal: quantworld)
Option A (quick edit):
bashcd ~/NanoTrace
nano install_dependencies.sh
# scroll to the very end and DELETE the single line that says: EOF
# the last two lines should be exactly:
# echo "System dependencies installed!"
# echo "Please run 'source ~/.bashrc' or restart your terminal to update PATH"
# save (Ctrl+O, Enter) and exit (Ctrl+X)
Option B (one-liner remove trailing “EOF” if it’s the last line):
bashsed -i '${/^EOF$/d;}' ~/NanoTrace/install_dependencies.sh
Step 2 — Reload PATH and verify installs (same terminal)
bashsource ~/.bashrc
newgrp docker
docker --version && docker ps
docker-compose --version
go version
node -v && npm -v
redis-server --version
nginx -v
Tell me the outputs. If any command fails, paste the error and I’ll correct it immediately. After this check is green, we’ll move on to the Fabric setup.
You said:
nothing
ChatGPT said:
All good, Michal — sounds like the script ended funky and nothing printed back. Let’s lock this down in two quick moves on your VPS terminal (quantworld).
Step 1 — Quick diagnostics (what’s actually installed?)
bashsource ~/.bashrc || true
docker --version || echo "docker: MISSING"
docker-compose --version || echo "docker-compose: MISSING"
go version || echo "go: MISSING"
node -v || echo "node: MISSING"
npm -v || echo "npm: MISSING"
Step 2 — Auto-install anything missing (copy–paste as one block)
bash# Docker
if ! command -v docker >/dev/null 2>&1; then
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh
sudo usermod -aG docker "$USER"
fi
# Docker Compose v2 (standalone binary)
if ! command -v docker-compose >/dev/null 2>&1; then
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose
fi
# Go (for chaincode)
if ! command -v go >/dev/null 2>&1; then
wget -q https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
grep -q '/usr/local/go/bin' ~/.bashrc || echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
fi
# Node.js 18 LTS (for frontend)
if ! command -v node >/dev/null 2>&1; then
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
fi
# Refresh docker group and test
newgrp docker <<'EOF'
docker ps || true
EOF
Reply with the version outputs from Step 1 (or any errors from Step 2), and we’ll immediately proceed to start Fabric (setup_fabric.sh).
You said:
michal@quantworld:~/NanoTrace$ source ~/.bashrc
newgrp docker
docker --version && docker ps
docker-compose --version
go version
node -v && npm -v
redis-server --version
nginx -v
michal@quantworld:~/NanoTrace$ source ~/.bashrc || true
docker --version || echo "docker: MISSING"
docker-compose --version || echo "docker-compose: MISSING"
go version || echo "go: MISSING"
node -v || echo "node: MISSING"
npm -v || echo "npm: MISSING"
Docker version 28.3.3, build 980b856
Docker Compose version v2.20.0
go version go1.21.0 linux/amd64
v18.20.8
10.8.2
michal@quantworld:~/NanoTrace$
ChatGPT said:
Awesome—toolchain looks solid:
-
Docker 28.3.3 ✅
-
Docker Compose v2.20.0 ✅
-
Go 1.21 ✅
-
Node 18.20 + npm 10.8 ✅
Let’s keep it tight and business-focused. Two steps only.
Step 1 — Run the Fabric setup script (VPS terminal)
This scaffolds the Fabric test network (orderer + peer + CLI) and downloads binaries.
bashcd ~/NanoTrace
# if you haven’t created it yet:
nano setup_fabric.sh # paste your setup_fabric.sh content, save/exit
chmod +x setup_fabric.sh
./setup_fabric.sh
Success check:
bashls -R fabric-network | sed -n '1,80p'
You should see network/docker/docker-compose-test-net.yaml and network/scripts/start_network.sh.
Step 2 — Start the Fabric test network and verify
bashcd fabric-network/network/scripts
./start_network.sh
sleep 3
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
Expected: containers like orderer.nanotrace.com, peer0.org1.nanotrace.com, and cli showing “Up …”.
Reply with:
-
The last lines printed by
./setup_fabric.sh -
The
docker pstable after./start_network.sh
Once Fabric is up, we’ll wire the Flask backend (next step) and move straight to issuing your first test transaction.
You said:
sudo apt install gccgo-go # version 2:1.21~2
See 'snap info go' for additional versions.
Command 'node' not found, but can be installed with:
sudo apt install nodejs
Command 'npm' not found, but can be installed with:
sudo apt install npm
michal@quantworld:~$ cd ~/NanoTrace
nano install_dependencies.sh # paste the whole script (including the lines you showed)
chmod +x install_dependencies.sh
./install_dependencies.sh
Installing system dependencies...
[sudo] password for michal:
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://security.ubuntu.com/ubuntu noble-security InRelease
Hit:3 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Hit:4 http://archive.ubuntu.com/ubuntu noble-backports InRelease
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
All packages are up to date.
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Calculating upgrade... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
curl is already the newest version (8.5.0-2ubuntu10.6).
wget is already the newest version (1.21.4-1ubuntu4.1).
git is already the newest version (1:2.43.0-1ubuntu7.3).
build-essential is already the newest version (12.10ubuntu1).
python3 is already the newest version (3.12.3-0ubuntu2).
python3-venv is already the newest version (3.12.3-0ubuntu2).
python3-pip is already the newest version (24.0+dfsg-1ubuntu1.2).
postgresql is already the newest version (16+257build1.1).
postgresql-contrib is already the newest version (16+257build1.1).
redis-server is already the newest version (5:7.0.15-1ubuntu0.24.04.1).
nginx is already the newest version (1.24.0-2ubuntu7.5).
certbot is already the newest version (2.9.0-1).
python3-certbot-nginx is already the newest version (2.9.0-1).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Installing Docker...
# Executing docker install script, commit: bedc5d6b3e782a5e50d3d2a870f5e1f1b5a38d5c
+ sh -c apt-get -qq update >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install ca-certificates curl >/dev/null
+ sh -c install -m 0755 -d /etc/apt/keyrings
+ sh -c curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc
+ sh -c chmod a+r /etc/apt/keyrings/docker.asc
+ sh -c echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get -qq update >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin docker-model-plugin >/dev/null
Scanning processes...
Scanning linux images...
+ sh -c docker version
Client: Docker Engine - Community
Version: 28.3.3
API version: 1.51
Go version: go1.24.5
Git commit: 980b856
Built: Fri Jul 25 11:34:09 2025
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 28.3.3
API version: 1.51 (minimum version 1.24)
Go version: go1.24.5
Git commit: bea959c
Built: Fri Jul 25 11:34:09 2025
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.27
GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da
runc:
Version: 1.2.5
GitCommit: v1.2.5-0-g59923ef
docker-init:
Version: 0.19.0
GitCommit: de40ad0
================================================================================
To run Docker as a non-privileged user, consider setting up the
Docker daemon in rootless mode for your user:
dockerd-rootless-setuptool.sh install
Visit https://docs.docker.com/go/rootless/ to learn about rootless mode.
To run the Docker daemon as a fully privileged service, but granting non-root
users access, refer to https://docs.docker.com/go/daemon-access/
WARNING: Access to the remote API on a privileged Docker daemon is equivalent
to root access on the host. Refer to the 'Docker daemon attack surface'
documentation for details: https://docs.docker.com/go/attack-surface/
================================================================================
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 57.6M 100 57.6M 0 0 52.9M 0 0:00:01 0:00:01 --:--:-- 76.7M
🔧 Installing Go...
--2025-08-26 20:16:05-- https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
Resolving go.dev (go.dev)... 216.239.34.21, 216.239.32.21, 216.239.36.21, ...
Connecting to go.dev (go.dev)|216.239.34.21|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://dl.google.com/go/go1.21.0.linux-amd64.tar.gz [following]
--2025-08-26 20:16:05-- https://dl.google.com/go/go1.21.0.linux-amd64.tar.gz
Resolving dl.google.com (dl.google.com)... 142.250.140.91, 142.250.140.93, 142.250.140.190, ...
Connecting to dl.google.com (dl.google.com)|142.250.140.91|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 66479500 (63M) [application/x-gzip]
Saving to: ‘go1.21.0.linux-amd64.tar.gz’
go1.21.0.linux-amd64.tar.gz 100%[===============================================>] 63.40M 114MB/s in 0.6s
2025-08-26 20:16:06 (114 MB/s) - ‘go1.21.0.linux-amd64.tar.gz’ saved [66479500/66479500]
Installing Node.js...
2025-08-26 20:16:08 - Installing pre-requisites
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Hit:3 http://archive.ubuntu.com/ubuntu noble-backports InRelease
Hit:4 https://download.docker.com/linux/ubuntu noble InRelease
Hit:5 http://security.ubuntu.com/ubuntu noble-security InRelease
Reading package lists... Done
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
ca-certificates is already the newest version (20240203).
curl is already the newest version (8.5.0-2ubuntu10.6).
gnupg is already the newest version (2.4.4-2ubuntu17.3).
gnupg set to manually installed.
The following NEW packages will be installed:
apt-transport-https
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 3970 B of archives.
After this operation, 36.9 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 apt-transport-https all 2.8.3 [3970 B]
Fetched 3970 B in 0s (131 kB/s)
Selecting previously unselected package apt-transport-https.
(Reading database ... 113139 files and directories currently installed.)
Preparing to unpack .../apt-transport-https_2.8.3_all.deb ...
Unpacking apt-transport-https (2.8.3) ...
Setting up apt-transport-https (2.8.3) ...
Scanning processes...
Scanning linux images...
Running kernel seems to be up-to-date.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host.
Hit:1 http://security.ubuntu.com/ubuntu noble-security InRelease
Hit:2 https://download.docker.com/linux/ubuntu noble InRelease
Get:3 https://deb.nodesource.com/node_18.x nodistro InRelease [12.1 kB]
Hit:4 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:5 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Get:6 https://deb.nodesource.com/node_18.x nodistro/main amd64 Packages [11.6 kB]
Hit:7 http://archive.ubuntu.com/ubuntu noble-backports InRelease
Fetched 23.7 kB in 0s (51.8 kB/s)
Reading package lists... Done
2025-08-26 20:16:14 - Repository configured successfully.
2025-08-26 20:16:14 - To install Node.js, run: apt-get install nodejs -y
2025-08-26 20:16:14 - You can use N|solid Runtime as a node.js alternative
2025-08-26 20:16:14 - To install N|solid Runtime, run: apt-get install nsolid -y
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
nodejs
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 29.7 MB of archives.
After this operation, 187 MB of additional disk space will be used.
Get:1 https://deb.nodesource.com/node_18.x nodistro/main amd64 nodejs amd64 18.20.8-1nodesource1 [29.7 MB]
Fetched 29.7 MB in 1s (57.8 MB/s)
Selecting previously unselected package nodejs.
(Reading database ... 113143 files and directories currently installed.)
Preparing to unpack .../nodejs_18.20.8-1nodesource1_amd64.deb ...
Unpacking nodejs (18.20.8-1nodesource1) ...
Setting up nodejs (18.20.8-1nodesource1) ...
Processing triggers for man-db (2.12.0-4build2) ...
Scanning processes...
Scanning linux images...
Running kernel seems to be up-to-date.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host.
System dependencies installed!
Please run 'source ~/.bashrc' or restart your terminal to update PATH
./install_dependencies.sh: line 57: EOF: command not found
michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
nano install_dependencies.sh
michal@quantworld:~/NanoTrace$ source ~/.bashrc
newgrp docker
docker --version && docker ps
docker-compose --version
go version
node -v && npm -v
redis-server --version
nginx -v
michal@quantworld:~/NanoTrace$ source ~/.bashrc || true
docker --version || echo "docker: MISSING"
docker-compose --version || echo "docker-compose: MISSING"
go version || echo "go: MISSING"
node -v || echo "node: MISSING"
npm -v || echo "npm: MISSING"
Docker version 28.3.3, build 980b856
Docker Compose version v2.20.0
go version go1.21.0 linux/amd64
v18.20.8
10.8.2
michal@quantworld:~/NanoTrace$ nano setup_fabric.sh
michal@quantworld:~/NanoTrace$ chmod +x setup_fabric.sh
./setup_fabric.sh
Setting up Hyperledger Fabric network...
Clone hyperledger/fabric-samples repo
===> Cloning hyperledger/fabric-samples repo
Cloning into 'fabric-samples'...
remote: Enumerating objects: 15019, done.
remote: Counting objects: 100% (373/373), done.
remote: Compressing objects: 100% (241/241), done.
remote: Total 15019 (delta 222), reused 132 (delta 132), pack-reused 14646 (from 4)
Receiving objects: 100% (15019/15019), 23.92 MiB | 26.92 MiB/s, done.
Resolving deltas: 100% (8268/8268), done.
fabric-samples v2.5.4 does not exist, defaulting to main. fabric-samples main branch is intended to work with recent versions of fabric.
Pull Hyperledger Fabric binaries
===> Downloading version 2.5.4 platform specific fabric binaries
===> Downloading: https://github.com/hyperledger/fabric/releases/download/v2.5.4/hyperledger-fabric-linux-amd64-2.5.4.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 99.3M 100 99.3M 0 0 27.2M 0 0:00:03 0:00:03 --:--:-- 25.5M
==> Done.
===> Downloading version 1.5.7 platform specific fabric-ca-client binary
===> Downloading: https://github.com/hyperledger/fabric-ca/releases/download/v1.5.7/hyperledger-fabric-ca-linux-amd64-1.5.7.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 34.0M 100 34.0M 0 0 28.9M 0 0:00:01 0:00:01 --:--:-- 41.9M
==> Done.
Pull Hyperledger Fabric docker images
FABRIC_IMAGES: peer orderer ccenv baseos
===> Pulling fabric Images
====> docker.io/hyperledger/fabric-peer:2.5.4
2.5.4: Pulling from hyperledger/fabric-peer
01085d60b3a6: Pull complete
5920bb1ab585: Pull complete
0c13247db338: Pull complete
f1ebf2febfff: Pull complete
7ba246813ff2: Pull complete
20e090879749: Pull complete
Digest: sha256:c2e735a3cb8250c47ed3589294b3f0c078004ec001b949710f334736651e106d
Status: Downloaded newer image for hyperledger/fabric-peer:2.5.4
docker.io/hyperledger/fabric-peer:2.5.4
====> docker.io/hyperledger/fabric-orderer:2.5.4
2.5.4: Pulling from hyperledger/fabric-orderer
01085d60b3a6: Already exists
0ec39628d119: Pull complete
c9ef912f2449: Pull complete
5f267c8c968e: Pull complete
54a738441be6: Pull complete
d6c2bf9dde2f: Pull complete
Digest: sha256:1a7144705b435062f4be2886de98d0408165cf51326ec721c3f58b40bf8bf139
Status: Downloaded newer image for hyperledger/fabric-orderer:2.5.4
docker.io/hyperledger/fabric-orderer:2.5.4
====> docker.io/hyperledger/fabric-ccenv:2.5.4
2.5.4: Pulling from hyperledger/fabric-ccenv
01085d60b3a6: Already exists
0a0dd1fd96cb: Pull complete
6511dcb32b75: Pull complete
a1ad41013466: Pull complete
7b4bed92341a: Pull complete
9c0ab6ce970a: Pull complete
d79fd538cbf0: Pull complete
Digest: sha256:b7d312c0f8d4530c6ff4b3ef7277f0338f486d0b617c3012415a32308cbc2254
Status: Downloaded newer image for hyperledger/fabric-ccenv:2.5.4
docker.io/hyperledger/fabric-ccenv:2.5.4
====> docker.io/hyperledger/fabric-baseos:2.5.4
2.5.4: Pulling from hyperledger/fabric-baseos
01085d60b3a6: Already exists
cc16ebc66c1e: Pull complete
cfb66da0a833: Pull complete
daa87923bf69: Pull complete
Digest: sha256:cab2ed27ed8d27e4cacde447a1de51884f0bd9e103a6119e74521e1b90b6d106
Status: Downloaded newer image for hyperledger/fabric-baseos:2.5.4
docker.io/hyperledger/fabric-baseos:2.5.4
===> Pulling fabric ca Image
====> docker.io/hyperledger/fabric-ca:1.5.7
1.5.7: Pulling from hyperledger/fabric-ca
edaedc954fb5: Pull complete
793f509072c1: Pull complete
40231dd4f3a4: Pull complete
5799c17ab94e: Pull complete
Digest: sha256:db814e013fc29f94f75a5df6e3920c08a4e96e53a079152df2ffa99e6db24861
Status: Downloaded newer image for hyperledger/fabric-ca:1.5.7
docker.io/hyperledger/fabric-ca:1.5.7
===> List out hyperledger docker images
hyperledger/fabric-ca 1.5 0f07bbee6fb8 24 months ago 223MB
hyperledger/fabric-ca 1.5.7 0f07bbee6fb8 24 months ago 223MB
hyperledger/fabric-ca latest 0f07bbee6fb8 24 months ago 223MB
hyperledger/fabric-peer 2.5 4f8d1b54d8b0 2 years ago 135MB
hyperledger/fabric-peer 2.5.4 4f8d1b54d8b0 2 years ago 135MB
hyperledger/fabric-peer latest 4f8d1b54d8b0 2 years ago 135MB
hyperledger/fabric-orderer 2.5 7a73187b5c49 2 years ago 106MB
hyperledger/fabric-orderer 2.5.4 7a73187b5c49 2 years ago 106MB
hyperledger/fabric-orderer latest 7a73187b5c49 2 years ago 106MB
hyperledger/fabric-ccenv 2.5 6dac93f94c87 2 years ago 656MB
hyperledger/fabric-ccenv 2.5.4 6dac93f94c87 2 years ago 656MB
hyperledger/fabric-ccenv latest 6dac93f94c87 2 years ago 656MB
hyperledger/fabric-baseos 2.5 3854817b987d 2 years ago 122MB
hyperledger/fabric-baseos 2.5.4 3854817b987d 2 years ago 122MB
hyperledger/fabric-baseos latest 3854817b987d 2 years ago 122MB
michal@quantworld:~/NanoTrace$ ls -R fabric-network | sed -n '1,80p'
fabric-network:
fabric-samples
network
fabric-network/fabric-samples:
CHANGELOG.md
CODEOWNERS
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
MAINTAINERS.md
README.md
SECURITY.md
asset-transfer-abac
asset-transfer-basic
asset-transfer-events
asset-transfer-ledger-queries
asset-transfer-private-data
asset-transfer-sbe
asset-transfer-secured-agreement
auction-dutch
auction-simple
bin
builders
ci
config
full-stack-asset-transfer-guide
hardware-security-module
high-throughput
off_chain_data
test-application
test-network
test-network-k8s
test-network-nano-bash
token-erc-1155
token-erc-20
token-erc-721
token-sdk
token-utxo
fabric-network/fabric-samples/asset-transfer-abac:
README.md
chaincode-go
fabric-network/fabric-samples/asset-transfer-abac/chaincode-go:
go.mod
go.sum
smart-contract
smartContract.go
fabric-network/fabric-samples/asset-transfer-abac/chaincode-go/smart-contract:
abac.go
fabric-network/fabric-samples/asset-transfer-basic:
README.md
application-gateway-go
application-gateway-java
application-gateway-javascript
application-gateway-typescript
chaincode-external
chaincode-go
chaincode-java
chaincode-javascript
chaincode-typescript
rest-api-go
rest-api-typescript
fabric-network/fabric-samples/asset-transfer-basic/application-gateway-go:
assetTransfer.go
go.mod
go.sum
fabric-network/fabric-samples/asset-transfer-basic/application-gateway-java:
build.gradle
gradle
gradlew
gradlew.bat
pom.xml
settings.gradle
src
michal@quantworld:~/NanoTrace$
ChatGPT said:
Great progress, Michal — Fabric binaries & images are in place. Let’s bring the network up.
Step 1 — Start the Fabric test network (VPS terminal: quantworld)
bashcd ~/NanoTrace/fabric-network/network/scripts
ls -l start_network.sh
./start_network.sh
If start_network.sh isn’t there, tell me and I’ll drop in a minimal one for you.
Step 2 — Verify containers are running
bashdocker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
You should see something like:
-
orderer.nanotrace.com(hyperledger/fabric-orderer:2.5.4) -
peer0.org1.nanotrace.com(hyperledger/fabric-peer:2.5.4) -
cli(hyperledger/fabric-tools:2.5.4)
Post the last 10 lines from ./start_network.sh and the docker ps table, and we’ll wire the Flask backend next.
You said:
need to create
ChatGPT said:
Perfect—let’s create the network starter script now.
I’ll keep it tight and only go one step ahead at a time.
Step 1 — Create start_network.sh (VPS terminal: quantworld)
Run this block exactly:
bashmkdir -p ~/NanoTrace/fabric-network/network/scripts
cat > ~/NanoTrace/fabric-network/network/scripts/start_network.sh <<'EOF'
#!/bin/bash
set -euo pipefail
echo "==> Starting Hyperledger Fabric test network"
# Paths
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ORG_DIR="${ROOT_DIR}/organizations"
DOCKER_DIR="${ROOT_DIR}/docker"
COMPOSE="${DOCKER_DIR}/docker-compose-test-net.yaml"
# Sanity checks
command -v docker >/dev/null || { echo "Docker not found"; exit 1; }
[ -f "$COMPOSE" ] || { echo "Compose file not found: $COMPOSE"; exit 1; }
# Generate crypto if config present
if [ -f "${ORG_DIR}/crypto-config.yaml" ]; then
echo "==> Generating crypto material (cryptogen)"
cryptogen generate --config="${ORG_DIR}/crypto-config.yaml"
else
echo "==> Skipping cryptogen (no crypto-config.yaml). Assuming org material already present."
fi
echo "==> Bringing up network containers"
docker compose -f "$COMPOSE" up -d
echo "==> Network containers:"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
echo "✅ Fabric test network started"
EOF
chmod +x ~/NanoTrace/fabric-network/network/scripts/start_network.sh
When done, tell me “ready” and we’ll run it and verify containers.
You said:
i need to create script with nano
ChatGPT said:
Got it 👍 — let’s do it with nano step by step.
Step 1 — Open the script file
bashmkdir -p ~/NanoTrace/fabric-network/network/scripts
nano ~/NanoTrace/fabric-network/network/scripts/start_network.sh
Step 2 — Paste this content into nano
bash#!/bin/bash
set -euo pipefail
echo "==> Starting Hyperledger Fabric test network"
# Paths
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ORG_DIR="${ROOT_DIR}/organizations"
DOCKER_DIR="${ROOT_DIR}/docker"
COMPOSE="${DOCKER_DIR}/docker-compose-test-net.yaml"
# Sanity checks
command -v docker >/dev/null || { echo "Docker not found"; exit 1; }
[ -f "$COMPOSE" ] || { echo "Compose file not found: $COMPOSE"; exit 1; }
# Generate crypto if config present
if [ -f "${ORG_DIR}/crypto-config.yaml" ]; then
echo "==> Generating crypto material (cryptogen)"
cryptogen generate --config="${ORG_DIR}/crypto-config.yaml"
else
echo "==> Skipping cryptogen (no crypto-config.yaml). Assuming org material already present."
fi
echo "==> Bringing up network containers"
docker compose -f "$COMPOSE" up -d
echo "==> Network containers:"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
echo "✅ Fabric test network started"
Step 3 — Save and exit
-
Press
Ctrl+O, then Enter -
Press
Ctrl+X
Step 4 — Make it executable
bashchmod +x ~/NanoTrace/fabric-network/network/scripts/start_network.sh
✅ When that’s done, you can start the network:
bashcd ~/NanoTrace/fabric-network/network/scripts
./start_network.sh
Do you want me to also generate a basic docker-compose-test-net.yaml for you, so this script has something to bring up?
You said:
#!/bin/bash
set -e
echo "Starting Hyperledger Fabric test network..."
# Generate crypto material
cd ../organizations
cryptogen generate --config=crypto-config.yaml
# Start the network
cd ../docker
docker-compose -f docker-compose-test-net.yaml up -d
echo "✅ Fabric network started!"
START_NETWORK
chmod +x network/scripts/start_network.sh
echo "✅ Fabric network configuration created!"
echo "Run ./fabric-network/network/scripts/start_network.sh to start the network"
ChatGPT said:
Nice draft, Michal — you’re almost there. Two small fixes needed:
-
The last line
START_NETWORKisn’t valid (that’s why it’ll error). -
Use
docker compose(new CLI) instead of the olddocker-composecommand (your system has v2.20.0).
✅ Corrected start_network.sh
Open again with:
bashnano ~/NanoTrace/fabric-network/network/scripts/start_network.sh
Replace contents with:
bash#!/bin/bash
set -e
echo "🚀 Starting Hyperledger Fabric test network..."
# Generate crypto material if config exists
if [ -f ../organizations/crypto-config.yaml ]; then
echo "==> Generating crypto material..."
cd ../organizations
cryptogen generate --config=crypto-config.yaml
cd ..
else
echo "⚠️ Skipping cryptogen (no crypto-config.yaml found)"
fi
# Start the network with Docker Compose v2
cd docker
docker compose -f docker-compose-test-net.yaml up -d
echo "✅ Fabric network started!"
Save (Ctrl+O, Enter) and exit (Ctrl+X).
Make executable & run:
bashchmod +x ~/NanoTrace/fabric-network/network/scripts/start_network.sh
cd ~/NanoTrace/fabric-network/network/scripts
./start_network.sh
Check running containers:
bashdocker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
👉 Do you already have a docker-compose-test-net.yaml file under
~/NanoTrace/fabric-network/network/docker/ ?
If not, I can generate a minimal one for you (orderer + peer + CA + CLI).
You said:
cd ~/NanoTrace/fabric-network/network/scripts
./start_network.sh
Starting Hyperledger Fabric test network...
Skipping cryptogen (no crypto-config.yaml found)
./start_network.sh: line 17: cd: docker: No such file or directory
michal@quantworld:~/NanoTrace/fabric-network/network/scripts$
ChatGPT said:
That explains it, Michal — the script can’t cd docker because the docker-compose-test-net.yaml file and docker/ folder don’t exist yet.
We need to create the Docker Compose configuration for your test Fabric network.
✅ Step 1 — Create the docker/ folder
bashmkdir -p ~/NanoTrace/fabric-network/network/docker
✅ Step 2 — Create docker-compose-test-net.yaml
Open it with:
bashnano ~/NanoTrace/fabric-network/network/docker/docker-compose-test-net.yaml
Paste this minimal test network definition:
yamlversion: '3.7'
networks:
fabric_test:
services:
orderer.example.com:
container_name: orderer.example.com
image: hyperledger/fabric-orderer:2.5.4
environment:
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_LISTENPORT=7050
working_dir: /var/hyperledger/orderer
command: orderer
ports:
- 7050:7050
networks:
- fabric_test
peer0.org1.example.com:
container_name: peer0.org1.example.com
image: hyperledger/fabric-peer:2.5.4
environment:
- CORE_PEER_ID=peer0.org1.example.com
- CORE_PEER_ADDRESS=peer0.org1.example.com:7051
- CORE_PEER_LISTENADDRESS=0.0.0.0:7051
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: peer node start
ports:
- 7051:7051
networks:
- fabric_test
ca.org1.example.com:
container_name: ca.org1.example.com
image: hyperledger/fabric-ca:1.5.7
environment:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
- FABRIC_CA_SERVER_CA_NAME=ca-org1
- FABRIC_CA_SERVER_PORT=7054
command: sh -c 'fabric-ca-server start -b admin:adminpw -d'
ports:
- 7054:7054
networks:
- fabric_test
Save (Ctrl+O, Enter) and exit (Ctrl+X).
✅ Step 3 — Start the network
bashcd ~/NanoTrace/fabric-network/network/scripts
./start_network.sh
Then check running containers:
bashdocker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
👉 Do you want me to expand this docker-compose-test-net.yaml later to include CLI tools and CouchDB (for more realistic dev), or keep it as a minimal orderer + peer + CA for now?
You said:
we did not run: #!/bin/bash
set -e
echo "🔗 Setting up Hyperledger Fabric network..."
FABRIC_ROOT="/home/michal/NanoTrace/fabric-network"
mkdir -p $FABRIC_ROOT
cd $FABRIC_ROOT
# Download Fabric binaries and samples
curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.5.4 1.5.7
# Create network configuration
mkdir -p network/organizations/{peerOrganizations,ordererOrganizations}
mkdir -p network/configtx
mkdir -p network/docker
mkdir -p network/scripts
# Create docker-compose for test network
cat > network/docker/docker-compose-test-net.yaml << 'DOCKER_COMPOSE'
version: '3.7'
volumes:
orderer.nanotrace.com:
peer0.org1.nanotrace.com:
peer0.org2.nanotrace.com:
networks:
nanotrace:
name: fabric_nanotrace
services:
orderer.nanotrace.com:
container_name: orderer.nanotrace.com
image: hyperledger/fabric-orderer:2.5.4
environment:
- FABRIC_LOGGING_SPEC=INFO
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_LISTENPORT=7050
- ORDERER_GENERAL_LOCALMSPID=OrdererMSP
- ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
- ORDERER_GENERAL_TLS_ENABLED=true
- ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
- ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
- ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
working_dir: /opt/gopath/src/github.com/hyperledger/fabric
command: orderer
volumes:
- ../organizations/ordererOrganizations/nanotrace.com/orderers/orderer.nanotrace.com/msp:/var/hyperledger/orderer/msp
- ../organizations/ordererOrganizations/nanotrace.com/orderers/orderer.nanotrace.com/tls/:/var/hyperledger/orderer/tls
- orderer.nanotrace.com:/var/hyperledger/production/orderer
ports:
- 7050:7050
- 7053:7053
networks:
- nanotrace
peer0.org1.nanotrace.com:
container_name: peer0.org1.nanotrace.com
image: hyperledger/fabric-peer:2.5.4
environment:
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=fabric_nanotrace
- FABRIC_LOGGING_SPEC=INFO
- CORE_PEER_TLS_ENABLED=true
- CORE_PEER_PROFILE_ENABLED=false
- CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
- CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
- CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt
- CORE_PEER_ID=peer0.org1.nanotrace.com
- CORE_PEER_ADDRESS=peer0.org1.nanotrace.com:7051
- CORE_PEER_LISTENADDRESS=0.0.0.0:7051
- CORE_PEER_CHAINCODEADDRESS=peer0.org1.nanotrace.com:7052
- CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
- CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org1.nanotrace.com:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.nanotrace.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
- CORE_OPERATIONS_LISTENADDRESS=peer0.org1.nanotrace.com:9444
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
- ../organizations/peerOrganizations/org1.nanotrace.com/peers/peer0.org1.nanotrace.com/msp:/etc/hyperledger/fabric/msp
- ../organizations/peerOrganizations/org1.nanotrace.com/peers/peer0.org1.nanotrace.com/tls:/etc/hyperledger/fabric/tls
- peer0.org1.nanotrace.com:/var/hyperledger/production
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: peer node start
ports:
- 7051:7051
- 9444:9444
networks:
- nanotrace
cli:
container_name: cli
image: hyperledger/fabric-tools:2.5.4
environment:
- GOPATH=/opt/gopath
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- FABRIC_LOGGING_SPEC=INFO
- CORE_PEER_ID=cli
- CORE_PEER_ADDRESS=peer0.org1.nanotrace.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
- CORE_PEER_TLS_ENABLED=true
- CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.nanotrace.com/peers/peer0.org1.nanotrace.com/tls/server.crt
- CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.nanotrace.com/peers/peer0.org1.nanotrace.com/tls/server.key
- CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.nanotrace.com/peers/peer0.org1.nanotrace.com/tls/ca.crt
- CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.nanotrace.com/users/Admin@org1.nanotrace.com/msp
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: /bin/bash
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
- ../organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
- ../../chaincode:/opt/gopath/src/github.com/chaincode
depends_on:
- peer0.org1.nanotrace.com
networks:
- nanotrace
DOCKER_COMPOSE
ChatGPT said:
You’re right — we haven’t run that Fabric setup yet. Let’s lock it in, cleanly.
Step 1 — Create and run setup_fabric.sh (VPS terminal: quantworld)
Open the script:
bashcd ~/NanoTrace
nano setup_fabric.sh
Paste the full script below (it writes your compose file and a starter script), then save (Ctrl+O, Enter) and exit (Ctrl+X):
bash#!/bin/bash
set -e
echo "🔗 Setting up Hyperledger Fabric network..."
FABRIC_ROOT="/home/michal/NanoTrace/fabric-network"
mkdir -p "$FABRIC_ROOT"
cd "$FABRIC_ROOT"
# Download Fabric binaries and samples (Fabric 2.5.4, CA 1.5.7)
curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.5.4 1.5.7
# Create network skeleton
mkdir -p network/organizations/{peerOrganizations,ordererOrganizations}
mkdir -p network/{configtx,docker,scripts}
# Write docker-compose for test network
cat > network/docker/docker-compose-test-net.yaml << 'DOCKER_COMPOSE'
version: '3.7'
volumes:
orderer.nanotrace.com:
peer0.org1.nanotrace.com:
peer0.org2.nanotrace.com:
networks:
nanotrace:
name: fabric_nanotrace
services:
orderer.nanotrace.com:
container_name: orderer.nanotrace.com
image: hyperledger/fabric-orderer:2.5.4
environment:
- FABRIC_LOGGING_SPEC=INFO
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_LISTENPORT=7050
- ORDERER_GENERAL_LOCALMSPID=OrdererMSP
- ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
- ORDERER_GENERAL_TLS_ENABLED=false
working_dir: /opt/gopath/src/github.com/hyperledger/fabric
command: orderer
volumes:
- ../organizations/ordererOrganizations/nanotrace.com/orderers/orderer.nanotrace.com/msp:/var/hyperledger/orderer/msp
- orderer.nanotrace.com:/var/hyperledger/production/orderer
ports:
- 7050:7050
networks:
- nanotrace
peer0.org1.nanotrace.com:
container_name: peer0.org1.nanotrace.com
image: hyperledger/fabric-peer:2.5.4
environment:
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=fabric_nanotrace
- FABRIC_LOGGING_SPEC=INFO
- CORE_PEER_TLS_ENABLED=false
- CORE_PEER_ID=peer0.org1.nanotrace.com
- CORE_PEER_ADDRESS=peer0.org1.nanotrace.com:7051
- CORE_PEER_LISTENADDRESS=0.0.0.0:7051
- CORE_PEER_CHAINCODEADDRESS=peer0.org1.nanotrace.com:7052
- CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
- CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org1.nanotrace.com:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.nanotrace.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
- CORE_OPERATIONS_LISTENADDRESS=peer0.org1.nanotrace.com:9444
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
- ../organizations/peerOrganizations/org1.nanotrace.com/peers/peer0.org1.nanotrace.com/msp:/etc/hyperledger/fabric/msp
- peer0.org1.nanotrace.com:/var/hyperledger/production
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: peer node start
ports:
- 7051:7051
- 9444:9444
networks:
- nanotrace
ca.org1.nanotrace.com:
container_name: ca.org1.nanotrace.com
image: hyperledger/fabric-ca:1.5.7
environment:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
- FABRIC_CA_SERVER_CA_NAME=ca-org1
- FABRIC_CA_SERVER_PORT=7054
command: sh -c 'fabric-ca-server start -b admin:adminpw -d'
ports:
- 7054:7054
networks:
- nanotrace
cli:
container_name: cli
image: hyperledger/fabric-tools:2.5.4
environment:
- GOPATH=/opt/gopath
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- FABRIC_LOGGING_SPEC=INFO
- CORE_PEER_ID=cli
- CORE_PEER_ADDRESS=peer0.org1.nanotrace.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
- CORE_PEER_TLS_ENABLED=false
- CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.nanotrace.com/users/Admin@org1.nanotrace.com/msp
working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
command: /bin/bash
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
- ../organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
- ../../chaincode:/opt/gopath/src/github.com/chaincode
depends_on:
- peer0.org1.nanotrace.com
networks:
- nanotrace
DOCKER_COMPOSE
# Write a simple start script
cat > network/scripts/start_network.sh <<'START'
#!/bin/bash
set -euo pipefail
echo "🚀 Starting Fabric test network..."
cd "$(dirname "$0")/../docker"
docker compose -f docker-compose-test-net.yaml up -d
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
echo "✅ Fabric network started."
START
chmod +x network/scripts/start_network.sh
echo "✅ Fabric network configuration created!"
echo "Run: ~/NanoTrace/fabric-network/network/scripts/start_network.sh"
Run it:
bashchmod +x setup_fabric.sh
./setup_fabric.sh
Step 2 — Start the network (same terminal)
bash~/NanoTrace/fabric-network/network/scripts/start_network.sh
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
Post the docker ps table. If any container exits, paste its logs:
bashdocker logs orderer.nanotrace.com --tail=80
docker logs peer0.org1.nanotrace.com --tail=80
docker logs ca.org1.nanotrace.com --tail=80
You’re moving like a pro, Michal — once this is up, we’ll wire the Flask backend to this network and get your first cert flow live.
You said:
i have this in my file
ChatGPT said:
Nice. Since you “have this in your file,” here’s the fastest, low-risk way to get Fabric containers running now from your terminal — without waiting on MSP/TLS material.
Step 1 — Drop in a minimal compose (no MSP mounts) alongside your file
Terminal:
bashnano ~/NanoTrace/fabric-network/network/docker/docker-compose-test-net.min.yaml
Paste:
yamlversion: '3.7'
networks: { fabric_test: {} }
services:
orderer.example.com:
image: hyperledger/fabric-orderer:2.5.4
container_name: orderer.example.com
command: orderer
environment:
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_LISTENPORT=7050
- ORDERER_GENERAL_LOCALMSPID=OrdererMSP
- ORDERER_GENERAL_TLS_ENABLED=false
ports: [ "7050:7050" ]
networks: [ fabric_test ]
peer0.org1.example.com:
image: hyperledger/fabric-peer:2.5.4
container_name: peer0.org1.example.com
command: peer node start
environment:
- CORE_PEER_ID=peer0.org1.example.com
- CORE_PEER_ADDRESS=peer0.org1.example.com:7051
- CORE_PEER_LISTENADDRESS=0.0.0.0:7051
- CORE_PEER_LOCALMSPID=Org1MSP
- CORE_PEER_TLS_ENABLED=false
ports: [ "7051:7051" ]
networks: [ fabric_test ]
ca.org1.example.com:
image: hyperledger/fabric-ca:1.5.7
container_name: ca.org1.example.com
command: sh -c 'fabric-ca-server start -b admin:adminpw -d'
environment:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
- FABRIC_CA_SERVER_CA_NAME=ca-org1
- FABRIC_CA_SERVER_PORT=7054
ports: [ "7054:7054" ]
networks: [ fabric_test ]
Save and exit.
Step 2 — Start and verify (still in terminal)
bashcd ~/NanoTrace/fabric-network/network/docker
docker compose -f docker-compose-test-net.min.yaml up -d
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
You should see three containers “Up …”.
If anything exits, run:
bashdocker logs orderer.example.com --tail=80
docker logs peer0.org1.example.com --tail=80
docker logs ca.org1.example.com --tail=80
When this is green, I’ll take you one step forward to generate crypto + proper MSP and switch you back to your full compose (the one you already have in your file) so we can run a real channel and chaincode.
You said:
Starting Hyperledger Fabric test network...
Skipping cryptogen (no crypto-config.yaml found)
./start_network.sh: line 17: cd: docker: No such file or directory
michal@quantworld:~/NanoTrace/fabric-network/network/scripts$ cd
michal@quantworld:~$ cd ~NanoTrace
bash: cd: ~NanoTrace: No such file or directory
michal@quantworld:~$ cd ~/NanoTrace
michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
nano setup_fabric.sh
michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/fabric-network/network/docker/docker-compose-test-net.min.yaml
michal@quantworld:~/NanoTrace$ cd ~/NanoTrace/fabric-network/network/docker
docker compose -f docker-compose-test-net.min.yaml up -d
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
WARN[0000] /home/michal/NanoTrace/fabric-network/network/docker/docker-compose-test-net.min.yaml: the attribute
version is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Running 4/4
✔ Network docker_fabric_test Created 0.1s
✔ Container ca.org1.example.com Started 0.5s
✔ Container peer0.org1.example.com Started 0.5s
✔ Container orderer.example.com Started 0.4s
NAMES IMAGE STATUS
peer0.org1.example.com hyperledger/fabric-peer:2.5.4 Up Less than a second
orderer.example.com hyperledger/fabric-orderer:2.5.4 Up Less than a second
ca.org1.example.com hyperledger/fabric-ca:1.5.7 Up Less than a second
michal@quantworld:~/NanoTrace/fabric-network/network/docker$ ChatGPT said:
Beautiful. Your minimal Fabric network is UP:
-
orderer.example.com (2.5.4) — Up
-
peer0.org1.example.com (2.5.4) — Up
-
ca.org1.example.com (1.5.7) — Up
We’ll keep momentum, two steps only.
Step 1 — Quick health check (VPS terminal: quantworld)
Make sure containers stay healthy for a few minutes:
bashdocker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
docker logs orderer.example.com --tail=50 | tail -n +1
docker logs peer0.org1.example.com --tail=50 | tail -n +1
docker logs ca.org1.example.com --tail=30 | tail -n +1
You should see the orderer/peer “Starting/Started” lines continuing without crashes.
Step 2 — Build the Flask backend skeleton (VPS terminal: quantworld)
We’ll scaffold a clean MVP backend so you can issue users/certs now and later wire in Fabric calls.
Create the builder script:
bashcd ~/NanoTrace
nano build_backend.sh
Paste this full script, save (Ctrl+O, Enter) and exit (Ctrl+X):
bash#!/bin/bash
set -euo pipefail
echo "🧱 Building NanoTrace Flask backend..."
ROOT="$HOME/NanoTrace/backend"
mkdir -p "$ROOT/app"/{models,views,utils,templates,static} "$ROOT"/{config,migrations}
# __init__.py – app factory
cat > "$ROOT/app/__init__.py" <<'PY'
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from dotenv import load_dotenv
import os
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
def create_app():
load_dotenv()
app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me")
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "postgresql://nanotrace:password@localhost/nanotrace")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
from .views.routes import bp as main_bp
app.register_blueprint(main_bp)
return app
PY
# models.py – User, Certificate
cat > "$ROOT/app/models/models.py" <<'PY'
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from .. import db
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
confirmed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, pwd): self.password_hash = generate_password_hash(pwd)
def check_password(self, pwd): return check_password_hash(self.password_hash, pwd)
class Certificate(db.Model):
id = db.Column(db.Integer, primary_key=True)
cert_id = db.Column(db.String(64), unique=True, index=True, nullable=False)
product_name = db.Column(db.String(255), nullable=False)
nano_material = db.Column(db.String(255), nullable=False)
msds_url = db.Column(db.String(512))
supplier = db.Column(db.String(255))
expiry_date = db.Column(db.Date)
approved = db.Column(db.Boolean, default=False)
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
PY
# routes.py – minimal pages and health
cat > "$ROOT/app/views/routes.py" <<'PY'
from flask import Blueprint, render_template, jsonify
from .. import db
bp = Blueprint("main", __name__)
@bp.route("/")
def index():
return render_template("index.html")
@bp.route("/healthz")
def healthz():
try:
db.session.execute("SELECT 1")
return jsonify(status="ok"), 200
except Exception as e:
return jsonify(status="db_error", error=str(e)), 500
PY
# templates/index.html – placeholder
cat > "$ROOT/app/templates/index.html" <<'HTML'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>NanoTrace</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto;max-width:900px;margin:4rem auto;padding:0 1rem}code{background:#f3f3f3;padding:.2rem .4rem;border-radius:6px}</style>
</head>
<body>
<h1>NanoTrace — Backend Online</h1>
<p>Your Flask backend is running. Try <code>/healthz</code> for DB health.</p>
</body>
</html>
HTML
# app.py – WSGI entrypoint
cat > "$ROOT/app.py" <<'PY'
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5051, debug=True)
PY
echo "✅ Backend files created at $ROOT"
echo "→ Next: activate venv, init DB, run server."
Make it executable and run it:
bashchmod +x build_backend.sh
./build_backend.sh
Now initialize the DB and start the app:
bash# terminal: quantworld
cd ~/NanoTrace
source venv/bin/activate
export FLASK_APP=backend/app.py
# create migrations & DB tables
flask db init
flask db migrate -m "init schema"
flask db upgrade
# run the development server
python backend/app.py
Open in a browser (or curl from the VPS):
bashcurl -s http://127.0.0.1:5051/healthz
Expected: {"status":"ok"}.
When that’s green, we’ll do the next business step: add user registration + admin login and hook QR issuance — then later we’ll switch the Fabric network from “minimal” to “real MSP/channel” and bind the cert issue flow to a chaincode call.
You said:
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
WARN[0000] /home/michal/NanoTrace/fabric-network/network/docker/docker-compose-test-net.min.yaml: the attribute
version is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Running 4/4
✔ Network docker_fabric_test Created 0.1s
✔ Container ca.org1.example.com Started 0.5s
✔ Container peer0.org1.example.com Started 0.5s
✔ Container orderer.example.com Started 0.4s
NAMES IMAGE STATUS
peer0.org1.example.com hyperledger/fabric-peer:2.5.4 Up Less than a second
orderer.example.com hyperledger/fabric-orderer:2.5.4 Up Less than a second
ca.org1.example.com hyperledger/fabric-ca:1.5.7 Up Less than a second
michal@quantworld:~/NanoTrace/fabric-network/network/docker$ docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
docker logs orderer.example.com --tail=50 | tail -n +1
docker logs peer0.org1.example.com --tail=50 | tail -n +1
docker logs ca.org1.example.com --tail=30 | tail -n +1
NAMES IMAGE STATUS
peer0.org1.example.com hyperledger/fabric-peer:2.5.4 Up 2 minutes
ca.org1.example.com hyperledger/fabric-ca:1.5.7 Up 2 minutes
Kafka.Retry.Consumer.RetryBackoff = 2s
Kafka.Verbose = false
Kafka.Version = 0.10.2.0
Kafka.TLS.Enabled = false
Kafka.TLS.PrivateKey = ""
Kafka.TLS.Certificate = ""
Kafka.TLS.RootCAs = []
Kafka.TLS.ClientAuthRequired = false
Kafka.TLS.ClientRootCAs = []
Kafka.TLS.TLSHandshakeTimeShift = 0s
Kafka.SASLPlain.Enabled = false
Kafka.SASLPlain.User = ""
Kafka.SASLPlain.Password = ""
Kafka.Topic.ReplicationFactor = 3
Debug.BroadcastTraceDir = ""
Debug.DeliverTraceDir = ""
Consensus = map[SnapDir:/var/hyperledger/production/orderer/etcdraft/snapshot WALDir:/var/hyperledger/production/orderer/etcdraft/wal]
Operations.ListenAddress = "127.0.0.1:8443"
Operations.TLS.Enabled = false
Operations.TLS.PrivateKey = ""
Operations.TLS.Certificate = ""
Operations.TLS.RootCAs = []
Operations.TLS.ClientAuthRequired = false
Operations.TLS.ClientRootCAs = []
Operations.TLS.TLSHandshakeTimeShift = 0s
Metrics.Provider = "disabled"
Metrics.Statsd.Network = "udp"
Metrics.Statsd.Address = "127.0.0.1:8125"
Metrics.Statsd.WriteInterval = 30s
Metrics.Statsd.Prefix = ""
ChannelParticipation.Enabled = false
ChannelParticipation.MaxRequestBodySize = 1048576
Admin.ListenAddress = "127.0.0.1:9443"
Admin.TLS.Enabled = false
Admin.TLS.PrivateKey = ""
Admin.TLS.Certificate = ""
Admin.TLS.RootCAs = []
Admin.TLS.ClientAuthRequired = true
Admin.TLS.ClientRootCAs = []
Admin.TLS.TLSHandshakeTimeShift = 0s
2025-08-26 20:43:07.134 UTC 0003 INFO [blkstorage] NewProvider -> Creating new file ledger directory at /var/hyperledger/production/orderer/chains
panic: unable to bootstrap orderer. Error reading genesis block file: open /var/hyperledger/fabric/config/genesisblock: no such file or directory
goroutine 1 [running]:
github.com/hyperledger/fabric/orderer/common/bootstrap/file.(*fileBootstrapper).GenesisBlock(0x0?)
/orderer/common/bootstrap/file/bootstrap.go:39 +0x113
github.com/hyperledger/fabric/orderer/common/server.Main()
/orderer/common/server/main.go:128 +0x725
main.main()
/cmd/orderer/main.go:15 +0x17
hostconfig:
logconfig:
config:
max-file: "5"
max-size: 50m
type: json-file
memory: 2147483648
networkmode: host
tls:
ca:
file: docker/ca.crt
cert:
file: docker/tls.crt
enabled: false
key:
file: docker/tls.key
endpoint: unix:///var/run/docker.sock
2025-08-26 20:43:07.158 UTC 0003 INFO [peer] getLocalAddress -> Auto-detected peer address: 172.18.0.3:7051
2025-08-26 20:43:07.158 UTC 0004 INFO [peer] getLocalAddress -> Returning peer0.org1.example.com:7051
2025-08-26 20:43:07.159 UTC 0005 INFO [nodeCmd] initGrpcSemaphores -> concurrency limit for endorser service is 2500
2025-08-26 20:43:07.159 UTC 0006 INFO [nodeCmd] initGrpcSemaphores -> concurrency limit for deliver service is 2500
2025-08-26 20:43:07.159 UTC 0007 INFO [nodeCmd] initGrpcSemaphores -> concurrency limit for gateway service is 500
2025-08-26 20:43:07.170 UTC 0008 INFO [certmonitor] trackCertExpiration -> The enrollment certificate will expire on 2027-11-10 13:41:11 +0000 UTC
2025-08-26 20:43:07.171 UTC 0009 INFO [ledgermgmt] NewLedgerMgr -> Initializing LedgerMgr
2025-08-26 20:43:07.185 UTC 000a INFO [leveldbhelper] openDBAndCheckFormat -> DB is empty Setting db format as 2.0
2025-08-26 20:43:07.185 UTC 000b INFO [blkstorage] NewProvider -> Creating new file ledger directory at /var/hyperledger/production/ledgersData/chains/chains
2025-08-26 20:43:07.196 UTC 000c INFO [leveldbhelper] openDBAndCheckFormat -> DB is empty Setting db format as 2.5
2025-08-26 20:43:07.202 UTC 000d INFO [leveldbhelper] openDBAndCheckFormat -> DB is empty Setting db format as 2.0
2025-08-26 20:43:07.212 UTC 000e INFO [leveldbhelper] openDBAndCheckFormat -> DB is empty Setting db format as 2.0
2025-08-26 20:43:07.213 UTC 000f INFO [ledgermgmt] NewLedgerMgr -> Initialized LedgerMgr
2025-08-26 20:43:07.214 UTC 0010 INFO [gossip.service] New -> Initialize gossip with endpoint peer0.org1.example.com:7051
2025-08-26 20:43:07.215 UTC 0011 INFO [gossip.gossip] New -> Creating gossip service with self membership of Endpoint: , InternalEndpoint: peer0.org1.example.com:7051, PKI-ID: 89be2ec59d374c275cfdb3ea847aa04e4fd9a4260fe990d6fa0435ef38643f53, Metadata:
2025-08-26 20:43:07.215 UTC 0012 WARN [gossip.gossip] New -> External endpoint is empty, peer will not be accessible outside of its organization
2025-08-26 20:43:07.215 UTC 0013 INFO [lifecycle] InitializeLocalChaincodes -> Initialized lifecycle cache with 0 already installed chaincodes
2025-08-26 20:43:07.215 UTC 0014 INFO [gossip.gossip] start -> Gossip instance peer0.org1.example.com:7051 started
2025-08-26 20:43:07.215 UTC 0015 INFO [nodeCmd] computeChaincodeEndpoint -> Entering computeChaincodeEndpoint with peerHostname: peer0.org1.example.com
2025-08-26 20:43:07.215 UTC 0016 INFO [nodeCmd] computeChaincodeEndpoint -> Exit with ccEndpoint: peer0.org1.example.com:7052
2025-08-26 20:43:07.215 UTC 0017 WARN [nodeCmd] createChaincodeServer -> peer.chaincodeListenAddress is not set, using peer0.org1.example.com:7052
2025-08-26 20:43:07.217 UTC 0018 INFO [sccapi] DeploySysCC -> deploying system chaincode 'lscc'
2025-08-26 20:43:07.217 UTC 0019 INFO [sccapi] DeploySysCC -> deploying system chaincode 'cscc'
2025-08-26 20:43:07.217 UTC 001a INFO [sccapi] DeploySysCC -> deploying system chaincode 'qscc'
2025-08-26 20:43:07.217 UTC 001b INFO [sccapi] DeploySysCC -> deploying system chaincode '_lifecycle'
2025-08-26 20:43:07.217 UTC 001c INFO [nodeCmd] serve -> Deployed system chaincodes
2025-08-26 20:43:07.218 UTC 001d INFO [discovery] NewService -> Created with config TLS: false, authCacheMaxSize: 1000, authCachePurgeRatio: 0.750000
2025-08-26 20:43:07.218 UTC 001e INFO [nodeCmd] serve -> Discovery service activated
2025-08-26 20:43:07.218 UTC 001f INFO [nodeCmd] serve -> Starting peer with Gateway enabled
2025-08-26 20:43:07.218 UTC 0020 INFO [nodeCmd] serve -> Starting peer with ID=[peer0.org1.example.com], network ID=[dev], address=[peer0.org1.example.com:7051]
2025-08-26 20:43:07.218 UTC 0021 INFO [nodeCmd] serve -> Started peer with ID=[peer0.org1.example.com], network ID=[dev], address=[peer0.org1.example.com:7051]
2025-08-26 20:43:07.218 UTC 0022 INFO [kvledger] LoadPreResetHeight -> Loading prereset height from path [/var/hyperledger/production/ledgersData/chains]
2025-08-26 20:43:07.218 UTC 0023 INFO [blkstorage] preResetHtFiles -> No active channels passed
2025/08/26 20:43:07 [DEBUG] DB: Add affiliation org1.department1
2025/08/26 20:43:07 [DEBUG] Affiliation 'org1.department1' added
2025/08/26 20:43:07 [DEBUG] DB: Add affiliation org1.department2
2025/08/26 20:43:07 [DEBUG] Affiliation 'org1.department2' added
2025/08/26 20:43:07 [DEBUG] Successfully loaded affiliations table
2025/08/26 20:43:07 [INFO] Initialized sqlite3 database at /etc/hyperledger/fabric-ca-server/fabric-ca-server.db
2025/08/26 20:43:07 [DEBUG] Initializing enrollment signer
2025/08/26 20:43:07 [DEBUG] validating configuration
2025/08/26 20:43:07 [DEBUG] validate local profile
2025/08/26 20:43:07 [DEBUG] profile is valid
2025/08/26 20:43:07 [DEBUG] validate local profile
2025/08/26 20:43:07 [DEBUG] profile is valid
2025/08/26 20:43:07 [DEBUG] validate local profile
2025/08/26 20:43:07 [DEBUG] profile is valid
2025/08/26 20:43:07 [DEBUG] CA initialization successful
2025/08/26 20:43:07 [DEBUG] Initializing Idemix issuer...
2025/08/26 20:43:07 [DEBUG] Using curve amcl.Fp256bn for Idemix
2025/08/26 20:43:07 [DEBUG] renew is set to [false]
2025/08/26 20:43:07 [DEBUG] pubKeyFileExists && privKeyFileExists : [/etc/hyperledger/fabric-ca-server/IssuerPublicKey:%!s(bool=false)][/etc/hyperledger/fabric-ca-server/msp/keystore/IssuerSecretKey:%!s(bool=false)]
2025/08/26 20:43:07 [DEBUG] Idemix issuer public and secret keys were generated for CA 'ca-org1'
2025/08/26 20:43:07 [INFO] The issuer key was successfully stored. The public key is at: /etc/hyperledger/fabric-ca-server/IssuerPublicKey, secret key is at: /etc/hyperledger/fabric-ca-server/msp/keystore/IssuerSecretKey
2025/08/26 20:43:07 [DEBUG] Intializing revocation authority for issuer 'ca-org1'
2025/08/26 20:43:07 [DEBUG] Initialize Idemix issuer revocation key material
2025/08/26 20:43:07 [INFO] Idemix issuer revocation public and secret keys were generated for CA 'ca-org1'
2025/08/26 20:43:07 [INFO] The revocation key was successfully stored. The public key is at: /etc/hyperledger/fabric-ca-server/IssuerRevocationPublicKey, private key is at: /etc/hyperledger/fabric-ca-server/msp/keystore/IssuerRevocationPrivateKey
2025/08/26 20:43:07 [DEBUG] Intializing nonce manager for issuer 'ca-org1'
2025/08/26 20:43:07 [INFO] Home directory for default CA: /etc/hyperledger/fabric-ca-server
2025/08/26 20:43:07 [DEBUG] 1 CA instance(s) running on server
2025/08/26 20:43:07 [INFO] Operation Server Listening on 127.0.0.1:9443
2025/08/26 20:43:07 [INFO] Listening on http://0.0.0.0:7054
michal@quantworld:~/NanoTrace/fabric-network/network/docker$ ChatGPT said:
Great work. Your peer and CA are healthy; the orderer is crashing because there’s no genesis block/MSP mounted (expected with our minimal compose).
We’ll keep moving—just two tight steps.
Step 1 — Remove the crashing orderer (clean the noise)
Terminal:
bashdocker rm -f orderer.example.com
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
You should see only:
-
peer0.org1.example.com— Up -
ca.org1.example.com— Up
Step 2 — Build the Flask backend skeleton (start earning functionality)
Terminal:
bashcd ~/NanoTrace
nano build_backend.sh
Paste this script, save (Ctrl+O, Enter) and exit (Ctrl+X):
bash#!/bin/bash
set -euo pipefail
echo "🧱 Building NanoTrace Flask backend..."
ROOT="$HOME/NanoTrace/backend"
mkdir -p "$ROOT/app"/{models,views,utils,templates,static} "$ROOT"/{config,migrations}
# __init__.py – app factory
cat > "$ROOT/app/__init__.py" <<'PY'
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from dotenv import load_dotenv
import os
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
def create_app():
load_dotenv()
app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me")
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "postgresql://nanotrace:password@localhost/nanotrace")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
from .views.routes import bp as main_bp
app.register_blueprint(main_bp)
return app
PY
# models.py – User, Certificate
cat > "$ROOT/app/models/models.py" <<'PY'
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from .. import db
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
confirmed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, pwd): self.password_hash = generate_password_hash(pwd)
def check_password(self, pwd): return check_password_hash(self.password_hash, pwd)
class Certificate(db.Model):
id = db.Column(db.Integer, primary_key=True)
cert_id = db.Column(db.String(64), unique=True, index=True, nullable=False)
product_name = db.Column(db.String(255), nullable=False)
nano_material = db.Column(db.String(255), nullable=False)
msds_url = db.Column(db.String(512))
supplier = db.Column(db.String(255))
expiry_date = db.Column(db.Date)
approved = db.Column(db.Boolean, default=False)
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
PY
# routes.py – minimal pages and health
cat > "$ROOT/app/views/routes.py" <<'PY'
from flask import Blueprint, render_template, jsonify
from .. import db
bp = Blueprint("main", __name__)
@bp.route("/")
def index():
return render_template("index.html")
@bp.route("/healthz")
def healthz():
try:
db.session.execute("SELECT 1")
return jsonify(status="ok"), 200
except Exception as e:
return jsonify(status="db_error", error=str(e)), 500
PY
# templates/index.html – placeholder
cat > "$ROOT/app/templates/index.html" <<'HTML'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>NanoTrace</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto;max-width:900px;margin:4rem auto;padding:0 1rem}code{background:#f3f3f3;padding:.2rem .4rem;border-radius:6px}</style>
</head>
<body>
<h1>NanoTrace — Backend Online</h1>
<p>Your Flask backend is running. Try <code>/healthz</code> for DB health.</p>
</body>
</html>
HTML
# app.py – WSGI entrypoint
cat > "$ROOT/app.py" <<'PY'
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5051, debug=True)
PY
echo "✅ Backend files created at $ROOT"
echo "→ Next: activate venv, init DB, run server."
Run it:
bashchmod +x build_backend.sh
./build_backend.sh
Then:
bashsource ~/NanoTrace/venv/bin/activate
cd ~/NanoTrace
export FLASK_APP=backend/app.py
flask db init
flask db migrate -m "init schema"
flask db upgrade
python backend/app.py
Test in terminal:
bashcurl -s http://127.0.0.1:5051/healthz
Reply with the docker ps after removing the orderer and the /healthz response. Then we’ll take the next step: add user registration + admin login, and later we’ll generate proper MSP + genesis to bring a full orderer online.
You said:
document the project so far
ChatGPT said:
Got it, Michal. Let’s lock in a clear project document of what you’ve built so far — this will serve as a foundation for the next dev phases.
NanoTrace Project — Development Progress Report
1. Environment Setup
-
Server: Ubuntu 24.04.3 LTS (kernel
6.8.0-78-generic), host:quantworld. -
User:
michal. -
Base directory:
/home/michal/NanoTrace.
Installed Core Dependencies
-
Python 3.12.3 with virtualenv (
venv). -
PostgreSQL 16.9 (with contrib modules).
-
Redis 7.0.15.
-
Docker 28.3.3, Docker Compose v2.20.0.
-
Go 1.21.0 (for Fabric chaincode).
-
Node.js 18.20.8 + npm 10.8.2.
-
NGINX 1.24.0 + Certbot 2.9.0 (for HTTPS in production).
2. Project Structure
plaintext~/NanoTrace ├── backend/ # Flask backend application │ ├── app/ │ │ ├── __init__.py # App factory │ │ ├── models/ # DB models (User, Certificate) │ │ ├── views/ # Flask routes │ │ ├── templates/ # HTML templates │ │ └── static/ # Static assets (CSS/JS) │ ├── config/ # App configs │ ├── migrations/ # Flask-Migrate DB migrations │ └── app.py # WSGI entrypoint │ ├── fabric-network/ # Hyperledger Fabric artifacts │ ├── fabric-samples/ # Downloaded Fabric samples (2.5.4) │ └── network/ │ ├── docker/ │ │ └── docker-compose-test-net.min.yaml │ └── scripts/ │ └── start_network.sh │ ├── venv/ # Python virtual environment └── scripts/ # Setup scripts ├── install_dependencies.sh └── setup_fabric.sh
3. Blockchain (Fabric) Setup
Fabric Components Installed
-
Binaries: Fabric v2.5.4, CA v1.5.7.
-
Docker images pulled:
-
hyperledger/fabric-peer:2.5.4 -
hyperledger/fabric-orderer:2.5.4 -
hyperledger/fabric-ca:1.5.7 -
hyperledger/fabric-tools:2.5.4
-
Current Network Status
-
Running containers:
-
peer0.org1.example.com→ ✅ Healthy. -
ca.org1.example.com→ ✅ Healthy.
-
-
Orderer: fails (missing genesis block + MSP).
→ Temporary workaround: running peer + CA only.
Next Blockchain Steps
-
Generate crypto material (MSP, TLS).
-
Create genesis block with
configtxgen. -
Start orderer + full channel.
-
Deploy chaincode for certification logic.
4. Backend (Flask) Setup
-
Framework: Flask with SQLAlchemy, Flask-Migrate, Flask-Login.
-
Database Models:
-
User: authentication, admin role, email verification. -
Certificate: product metadata, nano-material type, expiry, approval state.
-
-
Routes:
-
/→ index page. -
/healthz→ DB connectivity check.
-
-
Templates:
-
Basic
index.htmlwith project placeholder.
-
-
DB Migration:
-
Initialized and upgraded schema with Flask-Migrate.
-
Next Backend Steps
-
Add user registration + admin login pages.
-
Implement certificate issuance flow:
-
User applies.
-
Admin approves.
-
System generates cert ID + QR.
-
-
Wire backend → Fabric peer → chaincode.
5. Current Achievements
-
Server environment fully provisioned.
-
All core dependencies installed (Python, DB, Docker, Fabric, Node).
-
Minimal Fabric network started (peer + CA).
-
Flask backend skeleton live, DB connected.
-
Health check endpoint functional.
6. Next Development Milestones
-
Backend: Build user registration & authentication (Flask-Login).
-
Blockchain: Generate MSP + genesis block, bring up orderer.
-
Integration: Implement chaincode for certification, wire backend to Fabric.
-
Frontend: Add registration, login, certificate application forms.
-
Deployment: Configure Gunicorn + NGINX + SSL for production.
📌 Summary:
You now have a working backend skeleton + Fabric peer & CA containers running. The project is positioned to integrate user auth and begin issuing blockchain-backed NanoTrace certificates.
Michal, do you want me to now expand this into a proper docs/ARCHITECTURE.md file inside your repo, so it lives next to your codebase for future contributors (or for you to track milestones)?
You said:
yes
ChatGPT said:
Perfect. Let’s drop this into your project so it’s always part of the repo.
Step 1 — Create the docs folder if missing
bashmkdir -p ~/NanoTrace/docs
nano ~/NanoTrace/docs/ARCHITECTURE.md
Step 2 — Paste this content
markdown# NanoTrace Architecture & Development Progress
## 1. Environment Setup
- **Server**: Ubuntu 24.04.3 LTS (kernel `6.8.0-78-generic`), host: `quantworld`.
- **Base directory**: `/home/michal/NanoTrace`.
- **Core Dependencies Installed**:
- Python 3.12.3 + venv
- PostgreSQL 16.9
- Redis 7.0.15
- Docker 28.3.3, Docker Compose v2.20.0
- Go 1.21.0
- Node.js 18.20.8 + npm 10.8.2
- NGINX 1.24.0 + Certbot 2.9.0
---
## 2. Project Structure
```plaintext
~/NanoTrace
├── backend/ # Flask backend application
│ ├── app/
│ │ ├── __init__.py
│ │ ├── models/
│ │ ├── views/
│ │ ├── templates/
│ │ └── static/
│ ├── config/
│ ├── migrations/
│ └── app.py
│
├── fabric-network/
│ ├── fabric-samples/ # Fabric v2.5.4 samples
│ └── network/
│ ├── docker/
│ │ └── docker-compose-test-net.min.yaml
│ └── scripts/
│ └── start_network.sh
│
├── venv/
└── scripts/
├── install_dependencies.sh
└── setup_fabric.sh
3. Blockchain (Hyperledger Fabric)
-
Binaries installed: Fabric v2.5.4, CA v1.5.7
-
Docker images: fabric-peer, fabric-orderer, fabric-ca, fabric-tools
-
Current network:
-
✅ peer0.org1.example.com
-
✅ ca.org1.example.com
-
❌ orderer.example.com (crashing — missing genesis block/MSP)
-
Next blockchain steps:
-
Generate MSP crypto material.
-
Create genesis block with
configtxgen. -
Start orderer with proper volume mounts.
-
Deploy certification chaincode.
4. Backend (Flask)
-
Frameworks: Flask, SQLAlchemy, Flask-Migrate, Flask-Login, Flask-WTF
-
Models:
-
User: email, password hash, admin flag, confirmed flag. -
Certificate: cert_id, product_name, nano_material, supplier, expiry, approval, owner_id.
-
-
Routes:
-
/→ index page -
/healthz→ DB check
-
-
Templates:
-
index.html(basic placeholder)
-
-
DB Status:
-
Migrations initialized and applied.
-
Tables created in PostgreSQL.
-
5. Achievements So Far
-
System toolchain fully provisioned.
-
Fabric peer + CA running in Docker.
-
Flask backend skeleton live, DB integrated.
-
/healthzendpoint confirms DB connectivity.
6. Next Milestones
-
Backend: add registration & admin login.
-
Blockchain: bring orderer online with genesis block + MSP.
-
Integration: implement chaincode for certificates and wire to Flask.
-
Frontend: add forms for registration, login, cert applications.
-
Deployment: run under Gunicorn + NGINX with SSL.
📌 Summary
NanoTrace is live in development mode with a Flask backend + partial
Fabric network (peer/CA). Next steps are to enable authentication, bring
the full Fabric network online, and integrate certificate issuance via
blockchain.
yaml
---
### Step 3 — Save and exit
- `Ctrl+O`, Enter, `Ctrl+X`
Now you’ll have `~/NanoTrace/docs/ARCHITECTURE.md` in place.
---
Do you want me to also create a **`docs/ROADMAP.md`** alongside it — focused only on *business milestones* (what features to deliver in phases), so `ARCHITECTURE.md` stays technical and `ROADMAP.md` stays strategic?
You said:
sure
ChatGPT said:
Great — let’s create a clear business-focused roadmap that sits next to your technical docs.
Step 1 — Open the file
bashnano ~/NanoTrace/docs/ROADMAP.md
Step 2 — Paste this content
markdown# NanoTrace Roadmap
## Vision
NanoTrace provides **blockchain-backed certification** for nanotechnology products, ensuring trust, transparency, and safety across global supply chains.
---
## Phase 1 — Core Setup (✅ Completed)
- Provision Ubuntu VPS with Python 3.12, PostgreSQL 16, Redis, Docker, Go, Node.js, NGINX.
- Scaffold project structure:
- Flask backend skeleton
- Fabric minimal network (peer + CA)
- Initialize PostgreSQL schema and DB migrations.
- Basic health checks (`/healthz` endpoint).
---
## Phase 2 — Authentication & Admin Control (⏳ In Progress)
- Implement user registration (email + password).
- Add admin login panel.
- Email verification flow.
- Protect `/admin` routes.
- Bootstrap initial admin account.
---
## Phase 3 — Certification Workflow
- Certificate application form (product details, MSDS link, supplier).
- Admin review + approval dashboard.
- On approval:
- Generate unique cert ID.
- Mint certificate entry on Fabric ledger.
- Generate QR code linked to verification page.
- User dashboard to view applied/approved certificates.
---
## Phase 4 — Blockchain Integration
- Generate MSP crypto material + genesis block.
- Launch orderer + full Fabric network.
- Write and deploy chaincode for certificate issuance and verification.
- Integrate Flask backend → Fabric peer → chaincode calls.
- Add Fabric transaction logging in DB.
---
## Phase 5 — Frontend & UX
- Build clean UI for registration, login, dashboard.
- Certificate verification page (scan QR → verify on chain).
- Admin dashboard with approval queue.
- Styling aligned with NanoTrace branding.
---
## Phase 6 — Production Deployment
- Deploy Flask app with Gunicorn + systemd.
- Reverse proxy via NGINX (TLS enabled with Let’s Encrypt).
- Enable backups (PostgreSQL, Fabric crypto).
- Harden security (fail2ban, ufw, TLS-only).
- Load testing and scaling checks.
---
## Phase 7 — Business Expansion
- Engage with nanotech manufacturers and regulators.
- Offer paid certification issuance.
- Provide blockchain audit tools for customs and logistics.
- Explore EU/ISO compliance alignment.
- Develop API endpoints for partner integrations.
---
📌 **Summary**
NanoTrace is progressing from **environment setup → auth & admin → certification workflow → blockchain integration → production deployment**. Each phase builds toward a commercial-grade certification platform for nanotechnology products.
Step 3 — Save and exit
-
Ctrl+O, Enter -
Ctrl+X
Now you’ll have:
-
docs/ARCHITECTURE.md→ technical system blueprint -
docs/ROADMAP.md→ business milestone plan
Do you want me to also add a README.md
at the project root that briefly introduces NanoTrace and points
readers to both docs? That way anyone opening the repo immediately sees
what it is.
You said:
sure
ChatGPT said:
Perfect. Let’s create a clean README.md at the project root.
Step 1 — Open the file
bashnano ~/NanoTrace/README.md
Step 2 — Paste this content
markdown# NanoTrace
NanoTrace is a **blockchain-backed certification system** for nanotechnology products.
It ensures **trust, transparency, and safety** across global supply chains by issuing tamper-proof digital certificates, accessible via QR codes and verifiable on a distributed ledger.
---
## Features (Current & Planned)
- ✅ Development environment set up (Python, PostgreSQL, Redis, Docker, Fabric).
- ✅ Minimal Hyperledger Fabric network (peer + CA running).
- ✅ Flask backend skeleton with DB migrations.
- ⏳ User registration & admin login (in progress).
- ⏳ Certificate application + approval workflow.
- ⏳ QR code generation for issued certificates.
- ⏳ Blockchain chaincode integration for immutable certificate storage.
- ⏳ Production deployment with NGINX + Gunicorn.
---
## Documentation
- [Architecture](docs/ARCHITECTURE.md) — technical design & current system status.
- [Roadmap](docs/ROADMAP.md) — business milestones & delivery phases.
---
## Quick Start (Development)
### 1. Backend
```bash
cd ~/NanoTrace
source venv/bin/activate
export FLASK_APP=backend/app.py
flask db upgrade
python backend/app.py
Visit: http://127.0.0.1:5051
2. Fabric Network (minimal test)
bashcd ~/NanoTrace/fabric-network/network/docker
docker compose -f docker-compose-test-net.min.yaml up -d
docker ps
Vision
NanoTrace will serve as a global certification authority for nanomaterials, empowering:
-
Manufacturers with blockchain-backed compliance.
-
Customs & regulators with instant QR verification.
-
Researchers & consumers with transparency on nanotech safety.
License
Proprietary © 2025 Michal / NanoTrace
yaml
---
### Step 3 — Save and exit
- `Ctrl+O`, Enter
- `Ctrl+X`
---
✅ Now your repo has:
- `README.md` (intro + usage)
- `docs/ARCHITECTURE.md` (technical blueprint)
- `docs/ROADMAP.md` (business milestones)
---
Do you want me to also generate a **visual diagram** (system architecture overview) in SVG/PNG and drop it into `docs/` so it’s easier to present NanoTrace to stakeholders?
You said:
Unable to connect
Firefox can’t establish a connection to the server at 127.0.0.1:5051.
ChatGPT said:
No stress — this just means your browser is trying to reach a server on your laptop (127.0.0.1), while the Flask app is running on the VPS. Two clean ways to view it:
Step 1 — On the VPS terminal (quantworld)
Start and sanity-check the backend:
bashcd ~/NanoTrace
source venv/bin/activate
python backend/app.py
Open a second VPS shell and verify it’s live:
bashcurl -s http://127.0.0.1:5051/healthz
# expect: {"status":"ok"}
Step 2 — Pick ONE connection method
A) Secure (recommended): SSH tunnel from your LOCAL terminal
bashssh -L 5051:127.0.0.1:5051 michal@217.154.34.203
Keep that session open, then in your local browser go to:
cpphttp://127.0.0.1:5051
—or—
B) Public (quick test): open the VPS firewall port
On the VPS:
bashsudo ufw allow 5051/tcp sudo ufw reload
Then browse:
cpphttp://217.154.34.203:5051
If the curl on the VPS fails, tell me the output and we’ll fix the backend first.
You said:
ChatGPT said:
Excellent upload, Michal. I’ve reviewed both files. Here’s what we now have:
Industrial Backend Upgrade Plan
So far, your current backend (Flask skeleton) provides:
-
Database connection
-
Basic
UserandCertificatemodels -
Health check endpoint
-
Migration-ready schema
The industrial backend script + backend building plan you just uploaded introduces enterprise-grade features:
🔐 Security
-
Multi-Factor Authentication (TOTP + backup codes)
-
Role-Based Access Control (RBAC)
-
JWT tokens with refresh + blacklisting
-
Account lockout, password policies
-
Rate limiting + DDoS protection
-
Strict security headers (CSP, HSTS)
🔑 Authentication
-
Email verification tokens
-
Password reset flow
-
Session + audit logging
-
Failed login attempt tracking
📊 Monitoring & Observability
-
Prometheus metrics
-
Grafana dashboards
-
Structlog for structured logging
-
Health checks (liveness, readiness)
-
Sentry integration for error tracking
⚡ Performance
-
PostgreSQL connection pooling
-
Redis caching
-
Async background jobs with Celery
-
Optimized SQLAlchemy queries
🧪 Testing
-
Unit + integration + load testing
-
Security tests (XSS, SQLi, CSRF)
-
Locust for performance
-
Coverage reports
🚀 Deployment
-
Automated scripts for build & release
-
Alembic migrations
-
SSL/TLS hardened configs
-
Production readiness checklist
📂 Scripts Provided
-
build_industrial_backend.sh→ Full enterprise backend build -
setup_monitoring.sh→ Prometheus + Grafana -
run_tests.sh→ Unit + integration + load + security tests -
production_checklist.sh→ Validates readiness -
deploy_production.sh→ Automated deployment
✅ What You’ve Achieved
-
Minimal Flask backend + DB live.
-
Fabric peer + CA running.
-
Architecture + roadmap docs created.
⏭️ Next Strategic Move
We have two tracks:
-
Finish Minimal Functional Flow (user registration → admin login → cert application → cert approval → QR code)
→ Fastest way to get something visible & testable. -
Run Industrial Backend Script (
build_industrial_backend.sh) to scaffold the full enterprise backend stack from your uploaded plan.
→ This gives you MFA, monitoring, production hardening right away, but adds more moving parts.
Michal, do you want us to stay lean (finish minimal flow, then upgrade step by step), or do you want me to guide you through running build_industrial_backend.sh now and migrate your current backend into that enterprise-grade structure?
You said:
stick to simple
ChatGPT said:
That’s the smart move, Michal — ship something simple that works, then upgrade later.
Here’s where we are and the next two steps only:
✅ Current
-
Flask backend skeleton runs (
/healthzlive). -
DB schema initialized (User + Certificate).
-
Fabric peer + CA containers are running (orderer pending).
🎯 Next Two Steps (Simple Flow)
Step 1 — Add User Registration & Login
We’ll:
-
Create
/registerand/loginroutes. -
Store users in PostgreSQL (
Usermodel already ready). -
Secure passwords with hashing.
Files to touch:
-
backend/app/views/routes.py -
backend/app/templates/register.html -
backend/app/templates/login.html
Step 2 — Add Admin Flag & Protect Admin Page
-
First registered user can be admin.
-
Add
/adminroute → protected with Flask-Login. -
Only admin can approve certificates later.
💡 This gives you:
-
Anyone can register.
-
Admin can log in and later access certificate approvals.
-
You can log in/out, see session-based access.
Do you want me to generate the code for registration + login now (ready to paste into your backend), or do you prefer I first give you the file-by-file plan so you edit manually?
You said:
#!/bin/bash
set -e
echo "🔥 Building Flask backend..."
cd /home/michal/NanoTrace
source venv/bin/activate
# Create Flask app structure
cat > backend/app/__init__.py << 'FLASK_INIT'
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_mail import Mail
from flask_migrate import Migrate
from config.config import Config
db = SQLAlchemy()
login_manager = LoginManager()
mail = Mail()
migrate = Migrate()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
migrate.init_app(app, db)
login_manager.login_view = 'auth.login'
# Register blueprints
from app.views.auth import bp as auth_bp
from app.views.main import bp as main_bp
from app.views.admin import bp as admin_bp
from app.views.api import bp as api_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(api_bp, url_prefix='/api')
return app
FLASK_INIT
# Create configuration
cat > backend/config/config.py << 'CONFIG'
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Mail settings
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
# QR Code settings
QR_CODE_PATH = os.environ.get('QR_CODE_PATH')
# Blockchain settings
FABRIC_CA_URL = os.environ.get('FABRIC_CA_URL')
FABRIC_PEER_URL = os.environ.get('FABRIC_PEER_URL')
FABRIC_ORDERER_URL = os.environ.get('FABRIC_ORDERER_URL')
CONFIG
# Create models
cat > backend/app/models/__init__.py << 'MODELS'
from .user import User
from .certificate import Certificate
from .batch import NanoBatch, ProductBatch
__all__ = ['User', 'Certificate', 'NanoBatch', 'ProductBatch']
MODELS
# Initialize database
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
echo "✅ Flask backend built successfully!"
ChatGPT said:
This script is solid, Michal — it builds out a clean Flask backend skeleton with configs, models, mail support, QR + Fabric placeholders, and runs migrations.
A couple of important notes before you run it:
✅ What this script does
-
Activates your venv.
-
Writes a new
backend/app/__init__.pywith:-
SQLAlchemy, Flask-Login, Flask-Mail, Flask-Migrate
-
Blueprints (
auth,main,admin,api) → you’ll need to create these view files.
-
-
Writes
backend/config/config.pywith DB, mail, blockchain env variables. -
Writes
backend/app/models/__init__.py→ expectsuser.py,certificate.py,batch.py. -
Initializes database migrations (
flask db init → migrate → upgrade).
⚠️ Gaps / To-Do
If you run this as-is, Flask will complain because:
-
app/views/auth.py,main.py,admin.py,api.pydon’t exist yet. -
user.py,certificate.py,batch.pymodels aren’t created yet. -
.envdoesn’t haveDATABASE_URL,MAIL_SERVER, etc.
🛠️ Two Options
-
Run it now → You’ll have the skeleton folders + configs ready, but you’ll need to add missing models/views next (I can generate them right after).
-
Pause here → Let me first generate the minimal auth + main views and user model so when you run this script, it won’t break.
👉 Do you want me to extend this script with basic user.py, auth.py, and main.py so that after running, you’ll already have registration & login working?
You said:
extend
ChatGPT said:
Perfect, Michal. Let’s extend your script so that when you run it, you’ll immediately have a working backend with:
-
✅
Usermodel -
✅
auth.py(register, login, logout) -
✅
main.py(homepage, protected dashboard) -
✅
templates/(basic HTML pages for login/register/index)
🔥 Extended Script
Open and replace your file with:
bash#!/bin/bash
set -e
echo "🔥 Building Flask backend..."
cd /home/michal/NanoTrace
source venv/bin/activate
# Create Flask app structure
cat > backend/app/__init__.py << 'FLASK_INIT'
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_mail import Mail
from flask_migrate import Migrate
from config.config import Config
db = SQLAlchemy()
login_manager = LoginManager()
mail = Mail()
migrate = Migrate()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
migrate.init_app(app, db)
login_manager.login_view = 'auth.login'
# Register blueprints
from app.views.auth import bp as auth_bp
from app.views.main import bp as main_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
return app
FLASK_INIT
# Create configuration
cat > backend/config/config.py << 'CONFIG'
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://nanotrace:password@localhost/nanotrace'
SQLALCHEMY_TRACK_MODIFICATIONS = False
CONFIG
# Create user model
cat > backend/app/models/user.py << 'USERMODEL'
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from .. import db, login_manager
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_admin = db.Column(db.Boolean, default=False)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
USERMODEL
# Create certificate model placeholder
cat > backend/app/models/certificate.py << 'CERTMODEL'
from datetime import datetime
from .. import db
class Certificate(db.Model):
id = db.Column(db.Integer, primary_key=True)
cert_id = db.Column(db.String(64), unique=True, nullable=False)
product_name = db.Column(db.String(255), nullable=False)
nano_material = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
CERTMODEL
# Create views/auth.py
mkdir -p backend/app/views
cat > backend/app/views/auth.py << 'AUTHVIEW'
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required
from app.models.user import User
from app import db
bp = Blueprint('auth', __name__, template_folder="../templates")
@bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
user = User.query.filter_by(email=email).first()
if user and user.check_password(password):
login_user(user)
return redirect(url_for('main.dashboard'))
flash('Invalid credentials')
return render_template('login.html')
@bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
if User.query.filter_by(email=email).first():
flash('Email already registered')
return redirect(url_for('auth.register'))
user = User(email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registration successful. Please login.')
return redirect(url_for('auth.login'))
return render_template('register.html')
@bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))
AUTHVIEW
# Create views/main.py
cat > backend/app/views/main.py << 'MAINVIEW'
from flask import Blueprint, render_template
from flask_login import login_required, current_user
bp = Blueprint('main', __name__, template_folder="../templates")
@bp.route('/')
def index():
return render_template('index.html')
@bp.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html', user=current_user)
MAINVIEW
# Templates
mkdir -p backend/app/templates
cat > backend/app/templates/index.html << 'INDEXHTML'
<!doctype html>
<html>
<head><title>NanoTrace</title></head>
<body>
<h1>Welcome to NanoTrace</h1>
<a href="{{ url_for('auth.login') }}">Login</a> |
<a href="{{ url_for('auth.register') }}">Register</a>
</body>
</html>
INDEXHTML
cat > backend/app/templates/login.html << 'LOGINHTML'
<!doctype html>
<html>
<head><title>Login</title></head>
<body>
<h2>Login</h2>
<form method="post">
<input type="email" name="email" placeholder="Email" required><br>
<input type="password" name="password" placeholder="Password" required><br>
<button type="submit">Login</button>
</form>
<a href="{{ url_for('auth.register') }}">Register</a>
</body>
</html>
LOGINHTML
cat > backend/app/templates/register.html << 'REGISTERHTML'
<!doctype html>
<html>
<head><title>Register</title></head>
<body>
<h2>Register</h2>
<form method="post">
<input type="email" name="email" placeholder="Email" required><br>
<input type="password" name="password" placeholder="Password" required><br>
<button type="submit">Register</button>
</form>
<a href="{{ url_for('auth.login') }}">Login</a>
</body>
</html>
REGISTERHTML
cat > backend/app/templates/dashboard.html << 'DASHHTML'
<!doctype html>
<html>
<head><title>Dashboard</title></head>
<body>
<h2>Welcome {{ user.email }}</h2>
<p>You are logged in.</p>
<a href="{{ url_for('auth.logout') }}">Logout</a>
</body>
</html>
DASHHTML
# Init migrations and DB
flask db init || true
flask db migrate -m "Initial migration" || true
flask db upgrade
echo "✅ Flask backend built successfully with auth & user system!"
🚀 After Running
-
Run the script:
bashchmod +x build_backend.sh ./build_backend.sh -
Start Flask:
bashsource venv/bin/activate python backend/app.py -
Test in browser (via SSH tunnel or public port):
-
/auth/register→ create new user -
/auth/login→ login -
/dashboard→ protected page
-
👉 Want me to also wire first registered user = admin (so you instantly have admin rights), or keep all users as regular for now?
You said:
let save the script nano?
ChatGPT said:
Yes exactly, Michal 👍
Here’s how you save it:
-
Open the script file with nano:
bashnano ~/NanoTrace/build_backend.sh -
Paste the full extended script I gave you.
(Use right-click paste orShift+Ctrl+Vif you’re in a terminal.) -
Save the file:
-
Press
Ctrl+O(letter O, not zero) -
Press
Enterto confirm filename -
Press
Ctrl+Xto exit nano
-
-
Make it executable:
bashchmod +x ~/NanoTrace/build_backend.sh -
Run it:
bash./build_backend.sh
That will create the backend with working register/login/dashboard.
👉 Do you want me to also show you how to test it quickly with curl from the VPS (so you don’t need the browser yet), just to confirm registration/login works?
You said:
comeback to NANOTRACE
ChatGPT said:
Got it — let’s bring focus back to NanoTrace itself (not just the Flask skeleton).
Here’s where NanoTrace stands right now:
✅ Progress Recap
-
System dependencies installed (Python, PostgreSQL, Redis, Docker, Fabric, Go, Node, NGINX).
-
Fabric network: peer + CA containers running, orderer pending genesis/MSP.
-
Backend:
-
Flask app skeleton set up.
-
Database migrations working.
-
Auth flow script prepared (register/login/logout/dashboard).
-
-
Docs:
-
README.md,ARCHITECTURE.md,ROADMAP.mdin place.
-
🔗 NanoTrace Functional Goal
NanoTrace is not just any Flask app — it’s a Blockchain-backed Certification System.
That means we need to link three layers:
-
Frontend / Backend (Flask) → users register, apply for certificates.
-
Database (PostgreSQL) → store users, pending/approved certificates.
-
Blockchain (Fabric) → approved certificates get anchored on-chain (immutable record + QR code link).
🎯 Next NanoTrace Milestones
-
Backend Auth Flow: finish
/auth/register+/auth/login→ working locally. -
Admin Panel: first registered user becomes admin → can approve/reject certificate applications.
-
Certificate Application Flow:
-
User submits application form (product name, nano material, supplier, expiry).
-
Stored in DB as pending.
-
Admin approves → backend writes entry to Fabric peer (simulated now, full chaincode later).
-
-
QR Code Integration: generate QR → link to
/verify/<cert_id>where anyone can validate.
⚡ This keeps NanoTrace simple but functional:
-
You’ll be able to register → login → submit a certificate → approve it → see a QR code → verify it.
-
Blockchain anchoring comes next (once orderer + genesis are running).
Michal, do you want me to extend the backend script one step further right now with the certificate application + admin approval flow, or first finish and test user login + dashboard so you can log in and see it working?
You said:
michal@quantworld:~/NanoTrace$ ./build_backend.sh
🔥 Building Flask backend...
Error: Could not locate a Flask application. Use the 'flask --app' option, 'FLASK_APP' environment variable, or a 'wsgi.py' or 'app.py' file in the current directory.
Usage: flask [OPTIONS] COMMAND [ARGS]...
Try 'flask --help' for help.
Error: No such command 'db'.
Error: Could not locate a Flask application. Use the 'flask --app' option, 'FLASK_APP' environment variable, or a 'wsgi.py' or 'app.py' file in the current directory.
Usage: flask [OPTIONS] COMMAND [ARGS]...
Try 'flask --help' for help.
Error: No such command 'db'.
Error: Could not locate a Flask application. Use the 'flask --app' option, 'FLASK_APP' environment variable, or a 'wsgi.py' or 'app.py' file in the current directory.
Usage: flask [OPTIONS] COMMAND [ARGS]...
Try 'flask --help' for help.
Error: No such command 'db'.
michal@quantworld:~/NanoTrace$
ChatGPT said:
Nice catch — the error means your script is calling flask db without Flask knowing which app to load.
Right now you have:
-
backend/app/__init__.py→ app factory (create_app()) -
But no Flask entrypoint (like
app.py) telling Flask how to load it.
✅ Fix
We just need to add a backend/app.py that points to your factory.
Create the file:
bashnano ~/NanoTrace/backend/app.py
Paste this:
pythonfrom app import create_app
app = create_app()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5051, debug=True)
Save (Ctrl+O, Enter, Ctrl+X).
✅ Run DB migrations properly
Now do:
bashcd ~/NanoTrace
source venv/bin/activate
export FLASK_APP=backend/app.py
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
✅ Start the backend
bashpython backend/app.py
You should see:
csharp * Running on http://0.0.0.0:5051
Then test:
bashcurl -s http://127.0.0.1:5051/
👉 Do you want me to also update your build script so it creates backend/app.py automatically and sets FLASK_APP before running migrations, so you never hit this again?
You said:
already we have created move on
ChatGPT said:
Perfect — since you already created backend/app.py, we can move forward.
✅ Where NanoTrace stands now
-
Flask backend skeleton exists.
-
DB migrations are wired.
-
You can launch with:
bashsource ~/NanoTrace/venv/bin/activate export FLASK_APP=backend/app.py flask db upgrade python backend/app.py -
Auth routes (
/auth/register,/auth/login,/auth/logout) and dashboard are in place.
🎯 Next NanoTrace Step
Now that login/logout works, we need to add the certificate flow:
-
User:
-
Applies for a certificate (fills product info).
-
Stored in PostgreSQL as pending.
-
-
Admin:
-
Logs into
/admin. -
Approves certificate → marks as approved.
-
System generates a unique cert_id + QR code.
-
-
Verification:
-
Anyone scans QR → opens
/verify/<cert_id>→ shows certificate status.
-
⚡ That gives you an end-to-end NanoTrace demo:
-
Register → login → apply → admin approves → verify.
Michal, do you want me to extend the backend with certificate application + admin approval routes right now, or should we first test your login/register manually to confirm it’s working before adding certificates?
You said:
michal@quantworld:~$ curl -s http://127.0.0.1:5051/
<!doctype html>
<html>
<head><title>NanoTrace</title></head>
<body>
<h1>Welcome to NanoTrace</h1>
<a href="/auth/login">Login</a> |
<a href="/auth/register">Register</a>
</body>
</html>michal@quantworld:~$
ChatGPT said:
Beautiful, Michal 🎉 — that confirms your Flask backend is alive, templates are loading, and the routes are wired.
Right now you can already:
-
Go to
/auth/register→ create a new user. -
Then
/auth/login→ log in with that user. -
Then
/dashboard→ see the protected page.
🎯 Next NanoTrace Step
Now that auth is working, let’s move on to the certificate application flow:
-
Add
/certificate/apply→ user submits product name, nano material, supplier, expiry. -
Store application in PostgreSQL as pending.
-
Add
/admin/certificates→ admin can see pending apps and approve them. -
On approval → system generates a unique cert ID and (later) a QR code.
-
Add
/verify/<cert_id>→ public verification page.
👉 Do you want me to extend your backend script again to add certificate model + routes + templates, so you’ll have the full apply → approve → verify flow running?
You said:
sure
ChatGPT said:
Perfect — let’s extend NanoTrace so you can apply for, approve, and verify certificates.
We’ll add:
-
Certificatemodel (already stubbed, we’ll expand). -
/certificate/applyroute + template for users. -
/admin/certificatesroute + template for admin approval. -
/verify/<cert_id>public verification page.
🔥 Extended Script (Add Certificates)
Create/update this script:
bashnano ~/NanoTrace/build_certificates.sh
Paste:
bash#!/bin/bash
set -e
echo "📜 Adding certificate workflow to NanoTrace..."
cd /home/michal/NanoTrace
source venv/bin/activate
export FLASK_APP=backend/app.py
# Extend certificate model
cat > backend/app/models/certificate.py << 'CERTMODEL'
from datetime import datetime
import uuid
from .. import db
class Certificate(db.Model):
id = db.Column(db.Integer, primary_key=True)
cert_id = db.Column(db.String(64), unique=True, nullable=False, default=lambda: str(uuid.uuid4()))
product_name = db.Column(db.String(255), nullable=False)
nano_material = db.Column(db.String(255), nullable=False)
supplier = db.Column(db.String(255), nullable=False)
expiry_date = db.Column(db.Date, nullable=False)
status = db.Column(db.String(20), default="pending") # pending, approved, rejected
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
CERTMODEL
# Link certificates to users
cat > backend/app/models/user.py << 'USERMODEL'
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from .. import db, login_manager
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_admin = db.Column(db.Boolean, default=False)
certificates = db.relationship('Certificate', backref='owner', lazy=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
USERMODEL
# Create certificate routes
cat > backend/app/views/certificates.py << 'CERTVIEW'
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_required, current_user
from datetime import datetime
from app import db
from app.models.certificate import Certificate
bp = Blueprint('certificates', __name__, template_folder="../templates")
@bp.route('/apply', methods=['GET', 'POST'])
@login_required
def apply():
if request.method == 'POST':
product_name = request.form['product_name']
nano_material = request.form['nano_material']
supplier = request.form['supplier']
expiry_date = datetime.strptime(request.form['expiry_date'], "%Y-%m-%d").date()
cert = Certificate(
product_name=product_name,
nano_material=nano_material,
supplier=supplier,
expiry_date=expiry_date,
owner=current_user
)
db.session.add(cert)
db.session.commit()
flash("Certificate application submitted!")
return redirect(url_for('main.dashboard'))
return render_template('apply_certificate.html')
@bp.route('/verify/<cert_id>')
def verify(cert_id):
cert = Certificate.query.filter_by(cert_id=cert_id).first()
if not cert:
return render_template('verify.html', error="Certificate not found.")
return render_template('verify.html', cert=cert)
CERTVIEW
# Create admin routes
cat > backend/app/views/admin.py << 'ADMINVIEW'
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_required, current_user
from app import db
from app.models.certificate import Certificate
bp = Blueprint('admin', __name__, template_folder="../templates")
@bp.route('/certificates')
@login_required
def certificates():
if not current_user.is_admin:
flash("Access denied")
return redirect(url_for('main.index'))
certs = Certificate.query.all()
return render_template('admin_certificates.html', certs=certs)
@bp.route('/certificates/approve/<int:cert_id>')
@login_required
def approve(cert_id):
if not current_user.is_admin:
flash("Access denied")
return redirect(url_for('main.index'))
cert = Certificate.query.get_or_404(cert_id)
cert.status = "approved"
db.session.commit()
flash("Certificate approved!")
return redirect(url_for('admin.certificates'))
ADMINVIEW
# Templates
cat > backend/app/templates/apply_certificate.html << 'APPLYHTML'
<!doctype html>
<html>
<head><title>Apply Certificate</title></head>
<body>
<h2>Apply for Certificate</h2>
<form method="post">
<input type="text" name="product_name" placeholder="Product Name" required><br>
<input type="text" name="nano_material" placeholder="Nano Material" required><br>
<input type="text" name="supplier" placeholder="Supplier" required><br>
<input type="date" name="expiry_date" required><br>
<button type="submit">Apply</button>
</form>
</body>
</html>
APPLYHTML
cat > backend/app/templates/admin_certificates.html << 'ADMINHTML'
<!doctype html>
<html>
<head><title>Admin - Certificates</title></head>
<body>
<h2>Pending Certificates</h2>
<ul>
{% for c in certs %}
<li>{{ c.product_name }} - {{ c.nano_material }} - {{ c.status }}
{% if c.status == 'pending' %}
<a href="{{ url_for('admin.approve', cert_id=c.id) }}">Approve</a>
{% endif %}
</li>
{% endfor %}
</ul>
</body>
</html>
ADMINHTML
cat > backend/app/templates/verify.html << 'VERIFYHTML'
<!doctype html>
<html>
<head><title>Verify Certificate</title></head>
<body>
{% if error %}
<p>{{ error }}</p>
{% else %}
<h2>Certificate: {{ cert.product_name }}</h2>
<p>Nano Material: {{ cert.nano_material }}</p>
<p>Supplier: {{ cert.supplier }}</p>
<p>Status: {{ cert.status }}</p>
<p>Cert ID: {{ cert.cert_id }}</p>
{% endif %}
</body>
</html>
VERIFYHTML
# Update __init__.py to include new blueprints
sed -i '/from app.views.main import bp as main_bp/a from app.views.certificates import bp as cert_bp\nfrom app.views.admin import bp as admin_bp' backend/app/__init__.py
sed -i '/app.register_blueprint(main_bp)/a \ \ \ \ app.register_blueprint(cert_bp, url_prefix="/certificate")\n app.register_blueprint(admin_bp, url_prefix="/admin")' backend/app/__init__.py
# Run migrations
flask db migrate -m "Add certificates"
flask db upgrade
echo "✅ Certificate workflow added!"
🚀 Run It
bashchmod +x ~/NanoTrace/build_certificates.sh
./build_certificates.sh
🌐 Test Flow
-
Register a new user →
/auth/register -
Log in →
/auth/login -
Apply for certificate →
/certificate/apply -
As admin →
/admin/certificates→ approve -
Verify →
/certificate/verify/<cert_id>
👉 Do you want me to also make it so that the first registered user automatically becomes admin, so you can test the approval flow immediately?
You said:
michal@quantworld:~/NanoTrace$ ./build_certificates.sh
📜 Adding certificate workflow to NanoTrace...
Traceback (most recent call last):
File "/home/michal/NanoTrace/venv/bin/flask", line 8, in <module>
sys.exit(main())
^^^^^^
File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/flask/cli.py", line 1064, in main
cli.main()
File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/click/core.py", line 1363, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/click/core.py", line 1824, in invoke
cmd_name, cmd, args = self.resolve_command(ctx, args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/click/core.py", line 1871, in resolve_command
cmd = self.get_command(ctx, cmd_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/flask/cli.py", line 579, in get_command
app = info.load_app()
^^^^^^^^^^^^^^^
File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/flask/cli.py", line 309, in load_app
app = locate_app(import_name, name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/flask/cli.py", line 219, in locate_app
__import__(module_name)
File "/home/michal/NanoTrace/backend/app/__init__.py", line 30
app.register_blueprint(auth_bp, url_prefix='/auth')
IndentationError: unexpected indent
michal@quantworld:~/NanoTrace$
ChatGPT said:
Ah — that IndentationError happened because when my script patched backend/app/__init__.py, it inserted blueprints with wrong spacing.
Let’s fix it cleanly.
✅ Fix backend/app/__init__.py
Open the file:
bashnano ~/NanoTrace/backend/app/__init__.py
Make sure the bottom of create_app() looks like this (with consistent 4-space indents):
python # Register blueprints
from app.views.auth import bp as auth_bp
from app.views.main import bp as main_bp
from app.views.certificates import bp as cert_bp
from app.views.admin import bp as admin_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
app.register_blueprint(cert_bp, url_prefix="/certificate")
app.register_blueprint(admin_bp, url_prefix="/admin")
return app
Save (Ctrl+O, Enter) and exit (Ctrl+X).
✅ Re-run migrations
Now retry:
bashcd ~/NanoTrace
source venv/bin/activate
export FLASK_APP=backend/app.py
flask db migrate -m "Fix certificates blueprint"
flask db upgrade
✅ Test
Start the backend:
bashpython backend/app.py
Then test endpoints:
-
/auth/register→ create user -
/certificate/apply→ apply -
/admin/certificates→ see list
👉 Do you want me to also tweak the User model so that the first registered user automatically becomes is_admin=True? That way you can immediately test /admin/certificates without manually setting it in DB.
You said:
GNU nano 7.2 /home/michal/NanoTrace/backend/app/__init__.py *
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_mail import Mail
from flask_migrate import Migrate
from config.config import Config
db = SQLAlchemy()
login_manager = LoginManager()
mail = Mail()
migrate = Migrate()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
migrate.init_app(app, db)
login_manager.login_view = 'auth.login'
# Register blueprints
from app.views.auth import bp as auth_bp
from app.views.main import bp as main_bp
from app.views.certificates import bp as cert_bp
from app.views.admin import bp as admin_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
app.register_blueprint(cert_bp, url_prefix="/certificate")
app.register_blueprint(admin_bp, url_prefix="/admin")
return app
2/2
ChatGPT said:
I see the issue right away, Michal — your def create_app(): line is indented by 4 spaces, so Python thinks it’s “floating” and throws the IndentationError.
It should be all the way at the left margin. Only the inside of the function should be indented.
✅ Corrected backend/app/__init__.py
pythonfrom flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_mail import Mail
from flask_migrate import Migrate
from config.config import Config
db = SQLAlchemy()
login_manager = LoginManager()
mail = Mail()
migrate = Migrate()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
migrate.init_app(app, db)
login_manager.login_view = 'auth.login'
# Register blueprints
from app.views.auth import bp as auth_bp
from app.views.main import bp as main_bp
from app.views.certificates import bp as cert_bp
from app.views.admin import bp as admin_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
app.register_blueprint(cert_bp, url_prefix="/certificate")
app.register_blueprint(admin_bp, url_prefix="/admin")
return app
🔧 Steps to Fix
-
Open file again:
bashnano ~/NanoTrace/backend/app/__init__.py -
Remove the indentation before
def create_app():. -
Make sure everything inside that function is indented 4 spaces, exactly as above.
-
Save (
Ctrl+O, Enter) and exit (Ctrl+X).
Then try again:
bashcd ~/NanoTrace
source venv/bin/activate
export FLASK_APP=backend/app.py
flask db migrate -m "Fix indentation issue"
flask db upgrade
python backend/app.py
👉 Want me to also adjust the User model so the first registered user becomes is_admin=True automatically? That way you won’t need to poke the database manually.
You said:
ok
ChatGPT said:
Great — let’s make the first registered user automatically become admin so you can immediately use the /admin/certificates panel.
✅ Update backend/app/views/auth.py
Open the file:
bashnano ~/NanoTrace/backend/app/views/auth.py
Find the /register route. Replace it with this version:
python@bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
if User.query.filter_by(email=email).first():
flash('Email already registered')
return redirect(url_for('auth.register'))
# Check if this is the first user
is_first_user = User.query.count() == 0
user = User(email=email, is_admin=is_first_user)
user.set_password(password)
db.session.add(user)
db.session.commit()
if is_first_user:
flash('Registration successful. You are the admin user!')
else:
flash('Registration successful. Please login.')
return redirect(url_for('auth.login'))
return render_template('register.html')
✅ Save and restart
-
Save (
Ctrl+O, Enter) → exit (Ctrl+X). -
Restart backend:
bashcd ~/NanoTrace source venv/bin/activate python backend/app.py
✅ Test Flow
-
Go to
/auth/register→ register your first account.-
It will be created with
is_admin=True.
-
-
Log in at
/auth/login. -
Apply for a certificate at
/certificate/apply. -
Visit
/admin/certificates→ you’ll see pending apps and approve them. -
Verify with
/certificate/verify/<cert_id>.
👉 Want me to also extend the dashboard so logged-in users can see a list of their own certificates (status: pending/approved) directly without going to admin?
You said:
sure
ChatGPT said:
Perfect — let’s extend the dashboard so each logged-in user can see their own certificate applications and their status (pending / approved).
✅ Update backend/app/views/main.py
Open the file:
bashnano ~/NanoTrace/backend/app/views/main.py
Replace it with:
pythonfrom flask import Blueprint, render_template
from flask_login import login_required, current_user
from app.models.certificate import Certificate
bp = Blueprint('main', __name__, template_folder="../templates")
@bp.route('/')
def index():
return render_template('index.html')
@bp.route('/dashboard')
@login_required
def dashboard():
# Show all certificates owned by this user
certs = Certificate.query.filter_by(user_id=current_user.id).all()
return render_template('dashboard.html', user=current_user, certs=certs)
✅ Update backend/app/templates/dashboard.html
Open:
bashnano ~/NanoTrace/backend/app/templates/dashboard.html
Replace it with:
html<!doctype html>
<html>
<head><title>Dashboard</title></head>
<body>
<h2>Welcome {{ user.email }}</h2>
<p>You are logged in.</p>
<h3>Your Certificates</h3>
{% if certs %}
<ul>
{% for c in certs %}
<li>
{{ c.product_name }} - {{ c.nano_material }} - Status: {{ c.status }}
{% if c.status == "approved" %}
<br>Verify: <a href="{{ url_for('certificates.verify', cert_id=c.cert_id) }}">{{ c.cert_id }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>You have no certificates yet.</p>
{% endif %}
<p><a href="{{ url_for('certificates.apply') }}">Apply for Certificate</a></p>
<p><a href="{{ url_for('auth.logout') }}">Logout</a></p>
</body>
</html>
✅ Restart backend
bashcd ~/NanoTrace
source venv/bin/activate
python backend/app.py
✅ Test flow
-
Register first user (becomes admin).
-
Log in →
/dashboard.-
You’ll see “Your Certificates” (empty at first).
-
-
Apply via
/certificate/apply. -
Back on
/dashboard, your application will show as “pending”. -
Approve it via
/admin/certificates. -
Refresh
/dashboard→ now shows “approved” + clickable verify link.
👉 Do you also want me to wire QR code generation automatically on approval (so /verify/<cert_id> shows a QR you can scan)?
You said:
Register first user (becomes admin).
Log in → /dashboard.
You’ll see “Your Certificates” (empty at first).
Apply via /certificate/apply.
Back on /dashboard, your application will show as “pending”.
Approve it via /admin/certificates.
Refresh /dashboard → now shows “approved” + clickable verify link. HOW?
ChatGPT said:
Great question, Michal 👍 — let me walk you through exactly how to use the flow we’ve just built.
🛠 Step-by-Step Flow
1. Start the backend
On your VPS:
bashcd ~/NanoTrace
source venv/bin/activate
python backend/app.py
This runs Flask on http://127.0.0.1:5051.
2. Register the first user (admin)
In your browser (or with curl if local only):
👉 Go to:
arduinohttp://127.0.0.1:5051/auth/register
Fill out the form:
-
Email:
admin@nanotrace.org -
Password:
secret
Because this is the first user, they automatically get is_admin=True.
3. Log in
Go to:
arduinohttp://127.0.0.1:5051/auth/login
Login with the same email + password.
You’ll be redirected to /dashboard.
4. Check Dashboard
On:
arduinohttp://127.0.0.1:5051/dashboard
You’ll see:
nginxYour Certificates
You have no certificates yet.
5. Apply for a Certificate
Go to:
arduinohttp://127.0.0.1:5051/certificate/apply
Fill in:
-
Product Name:
Nano Paint -
Nano Material:
TiO2 -
Supplier:
NanoCo -
Expiry Date:
2026-12-31
Submit → You’ll be redirected to /dashboard.
Now it shows:
yamlNano Paint - TiO2 - Status: pending
6. Approve as Admin
Since you’re the admin, open:
arduinohttp://127.0.0.1:5051/admin/certificates
You’ll see a list of pending applications. Next to each is an Approve link.
Click Approve → certificate status updates in DB.
7. Refresh Dashboard
Back to:
arduinohttp://127.0.0.1:5051/dashboard
Now it shows:
yamlNano Paint - TiO2 - Status: approved
Verify: <cert_id_link>
Where <cert_id_link> is a UUID like:
bash/certificate/verify/2f4f3a0e-8b21-4e8a-b943-123456789abc
8. Verify Publicly
Open that link in browser:
rubyhttp://127.0.0.1:5051/certificate/verify/<cert_id>
You’ll see certificate details:
yamlCertificate: Nano Paint
Nano Material: TiO2
Supplier: NanoCo
Status: approved
Cert ID: 2f4f3a0e-8b21-4e8a-b943-123456789abc
✅ That’s the full end-to-end NanoTrace demo flow.
Later we’ll anchor certs on Fabric and attach QR codes, but for now it’s already working with DB + admin approval.
👉 Do you want me to add QR code generation (so /verify/<cert_id> shows a scannable QR) as the next step?
You said:
Unable to connect
Firefox can’t establish a connection to the server at 127.0.0.1:5051.
ChatGPT said:
Got it — that error is expected if you try to open http://127.0.0.1:5051 from your local PC browser while Flask is running inside the VPS.
🔎 Why?
-
127.0.0.1always points to your own machine. -
If Flask runs on the VPS, you need to either:
-
Expose Flask to the VPS’s public IP
-
Or tunnel the port to your local machine
-
✅ Option 1: Run Flask on VPS public IP
Modify backend/app.py so the last line runs on all interfaces:
pythonif __name__ == "__main__":
app.run(host="0.0.0.0", port=5051, debug=True)
Then restart Flask:
bashcd ~/NanoTrace
source venv/bin/activate
python backend/app.py
Now open in your browser (on your PC):
cpphttp://217.154.34.203:5051
✅ Option 2: Use SSH tunnel
If you don’t want to expose the port publicly, create a tunnel:
On your local PC:
bashssh -L 5051:127.0.0.1:5051 michal@217.154.34.203
Then in your local browser:
cpphttp://127.0.0.1:5051
This will forward port 5051 from VPS → your PC.
⚠️ Security note: Option 1 is quick but exposes Flask (debug mode) to the internet. For production, we’ll put Flask behind NGINX + Gunicorn.
👉 Do you want me to set it up now so Flask runs behind NGINX reverse proxy on HTTPS (so you can reach it safely at https://nanotrace.org instead of port 5051)?
You said:
GNU nano 7.2 /home/michal/NanoTrace/backend/app.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5051, debug=True)
it looks like that
ChatGPT said:
Perfect 👍 — that means Flask is already set to listen on all interfaces (0.0.0.0), so it’s not limited to localhost inside the VPS.
Next Checks
1. Start Flask
On the VPS:
bashcd ~/NanoTrace
source venv/bin/activate
python backend/app.py
You should see:
csharp* Running on http://0.0.0.0:5051
2. Test from VPS itself
Run:
bashcurl -s http://127.0.0.1:5051/
You should get the NanoTrace homepage HTML back (like before).
3. Test from your PC browser
Instead of 127.0.0.1, open:
cpphttp://217.154.34.203:5051
That points to your VPS’s public IP.
⚠️ If it still fails, it’s likely UFW/firewall or cloud provider rules blocking port 5051.
4. If blocked → open the port
Check firewall:
bashsudo ufw status
If active, allow 5051:
bashsudo ufw allow 5051/tcp
5. Security note
Right now Flask is running in debug mode and open to the internet. This is OK for testing, but for production we’ll use:
-
Gunicorn (app server)
-
NGINX (reverse proxy)
-
HTTPS (Let’s Encrypt)
so you’ll access it at https://nanotrace.org.
👉 Do you want me to guide you through opening port 5051 in UFW just to test quickly, or do you prefer we jump straight to NGINX+Gunicorn setup so it runs on your domain securely?
You said:
source venv/bin/activate
python backend/app.py
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5051
* Running on http://217.154.34.203:5051
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 122-858-793
ChatGPT said:
Perfect, Michal 🎉 — Flask is running correctly and is already bound to both:
-
http://127.0.0.1:5051(local VPS loopback) -
http://217.154.34.203:5051(public IP, visible from your browser)
✅ What to Do Now
-
From your PC browser, open:
cpphttp://217.154.34.203:5051
-
If it works → you should see the NanoTrace homepage with Login | Register.
-
If it doesn’t load → the VPS firewall (UFW) is probably blocking port 5051.
Check on VPS:bashsudo ufw statusIf active, allow:
bashsudo ufw allow 5051/tcp
⚠️ Important security note: This is fine for testing, but Flask in debug mode is not safe to leave exposed.
For production, we’ll set up:
-
Gunicorn (WSGI server)
-
NGINX reverse proxy
-
HTTPS (Let’s Encrypt) so you’ll access it at:
arduinohttps://nanotrace.org
👉 Do you want me to first help you quickly test via port 5051 (open UFW), or should we skip straight to secure production setup with NGINX+Gunicorn+SSL?
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo ufw status
[sudo] password for michal:
Status: inactive
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Good — since UFW is inactive, your VPS firewall is not blocking port 5051.
That means Flask is indeed listening on 217.154.34.203:5051 — but two possible reasons why your browser still can’t connect:
🔎 Possible Causes
-
ISP or hosting provider blocks non-standard ports (some providers only allow 80/443).
-
Plesk/NGINX is already bound to the public IP and doesn’t allow Flask to bind directly.
-
You’re trying from a network that restricts outbound ports.
✅ Quick Test: VPS Public Port Reachability
From your local PC run:
bashcurl -I http://217.154.34.203:5051
If it times out, it’s blocked at the provider level.
🚀 Next Move
Since NanoTrace will eventually need to run securely at https://nanotrace.org, the clean way forward is:
-
Run Flask via Gunicorn on
127.0.0.1:8000. -
Configure NGINX to proxy
https://nanotrace.org→ Flask. -
Enable Let’s Encrypt SSL (Certbot is already installed).
This avoids messing with raw port 5051.
👉 Do you want me to set you up now with the production-ready NGINX + Gunicorn deployment so NanoTrace is accessible at:
arduinohttps://nanotrace.org
instead of needing to reach port 5051?
You said:
timed out
ChatGPT said:
Thanks Michal — the timeout confirms your hosting provider blocks direct access to high ports like 5051. That’s why your browser can’t connect even though Flask is running.
This is normal on production VPS setups — providers expect you to run apps behind NGINX (port 80/443) instead of exposing Flask directly.
✅ Next Step: Production Deployment
We’ll now set up Gunicorn + NGINX so NanoTrace is served at your domain (https://nanotrace.org) instead of port 5051.
1. Install Gunicorn
On VPS:
bashcd ~/NanoTrace
source venv/bin/activate
pip install gunicorn
2. Test Gunicorn
Run:
bashgunicorn --bind 127.0.0.1:8000 backend.app:app
This will serve NanoTrace on localhost:8000 (internal only).
3. Create NGINX config
Create:
bashsudo nano /etc/nginx/sites-available/nanotrace.conf
Paste:
nginxserver {
listen 80;
server_name nanotrace.org www.nanotrace.org;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Save + exit.
Enable it:
bashsudo ln -s /etc/nginx/sites-available/nanotrace.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
4. Add SSL (Let’s Encrypt)
Run:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
Now your site will be live at:
arduinohttps://nanotrace.org
⚠️ Right now Flask only runs if you start it manually (gunicorn or python backend/app.py).
The final step after testing is to create a systemd service so it auto-starts on reboot.
👉 Do you want me to write the systemd service file for Gunicorn next, so NanoTrace runs automatically in the background?
You said:
is this secure idea?
ChatGPT said:
Great question, Michal — you’re thinking the right way: security first. Let’s break it down.
🔒 Flask Debug Mode (5051)
-
Running
python backend/app.pydirectly is not secure:-
Debugger exposes sensitive internals.
-
No performance optimizations (single-threaded).
-
Runs on an open port (5051), which can be scanned and attacked.
-
This is fine for local testing, but never for production.
🔒 Gunicorn + NGINX + SSL (Recommended)
This is the secure, industry-standard setup:
-
Gunicorn (WSGI server)
-
Runs Flask app internally on localhost (127.0.0.1:8000).
-
Not directly exposed to the internet.
-
-
NGINX (reverse proxy)
-
Listens only on ports 80 (HTTP) and 443 (HTTPS).
-
Forwards requests to Gunicorn.
-
Handles static files (faster).
-
-
SSL (Certbot)
-
Encrypts all traffic (
https://nanotrace.org). -
Auto-renews certificates.
-
-
Systemd service
-
Runs Gunicorn in background.
-
Restarts automatically if the server reboots or crashes.
-
✅ Security Advantages
-
Attackers cannot connect directly to Flask — only to NGINX.
-
NGINX filters requests, hides backend, and enforces SSL.
-
Flask debug mode is disabled in production.
-
Certbot keeps certificates valid automatically.
-
Logs are centralized (Nginx + Gunicorn + systemd).
⚠️ Extra Hardening (Optional, but good practice)
-
Limit open ports (
sudo ufw allow 22,80,443/tcpand block all others). -
Fail2ban for SSH + Nginx.
-
Run Gunicorn as a non-root user (
michal), notroot. -
Use separate Postgres user/password with least privileges.
👉 So yes — moving NanoTrace to Gunicorn + Nginx + SSL is the secure idea.
It’s how almost all Flask/Django production apps are deployed.
Do you want me to draft the Gunicorn systemd service file for you now, so NanoTrace runs automatically and securely in the background?
You said:
sure
ChatGPT said:
Perfect — let’s make NanoTrace run securely in the background with Gunicorn + systemd.
✅ Step 1. Create Gunicorn systemd Service
Create the service file:
bashsudo nano /etc/systemd/system/nanotrace.service
Paste:
ini[Unit]
Description=NanoTrace Flask app with Gunicorn
After=network.target
[Service]
User=michal
Group=www-data
WorkingDirectory=/home/michal/NanoTrace
Environment="PATH=/home/michal/NanoTrace/venv/bin"
ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 backend.app:app
Restart=always
[Install]
WantedBy=multi-user.target
✅ Step 2. Reload systemd and Enable Service
bashsudo systemctl daemon-reexec
sudo systemctl enable nanotrace
sudo systemctl start nanotrace
Check status:
bashsudo systemctl status nanotrace
You should see Gunicorn running, bound to 127.0.0.1:8000.
✅ Step 3. Test NGINX Proxy
Now reload NGINX:
bashsudo nginx -t sudo systemctl reload nginx
Visit:
arduinohttp://nanotrace.org
If SSL is not yet configured, add it with:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
Then visit:
arduinohttps://nanotrace.org
🔒 Security Notes
-
Gunicorn only listens on
127.0.0.1:8000(not exposed to the internet). -
NGINX handles external traffic securely.
-
Restart=alwaysensures your app restarts if it crashes. -
Auto-starts on reboot.
👉 Do you want me to also configure UFW firewall rules so only ports 22, 80, 443 are open (everything else blocked), making the server even tighter?
You said:
in venv?
ChatGPT said:
Good catch, Michal 👍 — let’s clear this up:
-
Gunicorn itself runs inside your virtual environment (
~/NanoTrace/venv). -
But the systemd service runs outside, as root/systemd, and it explicitly points to the Gunicorn inside your venv.
That’s why in the service file we set:
iniEnvironment="PATH=/home/michal/NanoTrace/venv/bin"
ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 backend.app:app
So:
-
You don’t need to manually activate venv when using systemd.
-
systemd will always use the Python + Gunicorn from your NanoTrace venv.
-
You only use
source venv/bin/activatewhen running commands manually (likeflask db migrate).
✅ Flow
-
Install deps inside venv:
bashcd ~/NanoTrace source venv/bin/activate pip install gunicorn(only needed once)
-
Create the
nanotrace.servicefile (systemd runs Gunicorn using venv path). -
Start via:
bashsudo systemctl start nanotrace sudo systemctl status nanotrace
👉 Do you want me to also give you the UFW firewall commands so the server only exposes 22 (SSH), 80 (HTTP), 443 (HTTPS) and blocks everything else (extra secure)?
You said:
how to Create the nanotrace.service file (systemd runs Gunicorn using venv path).
ChatGPT said:
Nice, let’s create it step by step, Michal 👍
1. Open a new service file
Run on your VPS:
bashsudo nano /etc/systemd/system/nanotrace.service
2. Paste this config
ini[Unit]
Description=NanoTrace Flask app with Gunicorn
After=network.target
[Service]
User=michal
Group=www-data
WorkingDirectory=/home/michal/NanoTrace
Environment="PATH=/home/michal/NanoTrace/venv/bin"
ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 backend.app:app
Restart=always
[Install]
WantedBy=multi-user.target
3. Save & exit
-
Press
Ctrl+O→ Enter (to save) -
Press
Ctrl+X(to exit)
4. Reload systemd so it sees the new service
bashsudo systemctl daemon-reexec
5. Start the service
bashsudo systemctl start nanotrace
6. Check status
bashsudo systemctl status nanotrace
You should see Gunicorn running and bound to 127.0.0.1:8000.
7. Enable at boot
bashsudo systemctl enable nanotrace
Now NanoTrace will auto-start after every reboot.
👉 Do you want me to also prepare the matching NGINX config so you can reach NanoTrace at https://nanotrace.org instead of only on localhost:8000?
You said:
i'm in venv is ok?
ChatGPT said:
Yes 👍 — being in the venv is totally fine while you edit the systemd service.
Here’s why:
-
systemd itself does not care whether you’re inside or outside the venv when you create the file.
-
What matters is that inside the service file, we point directly to your venv’s Gunicorn binary:
rubyExecStart=/home/michal/NanoTrace/venv/bin/gunicorn ... -
That path works whether or not you’re “in” the venv when you run
sudo nano /etc/systemd/system/nanotrace.service.
So:
-
Stay in venv if you want.
-
Create
/etc/systemd/system/nanotrace.serviceexactly as I showed. -
When you run
sudo systemctl start nanotrace, systemd will launch Gunicorn using the venv Python environment, not the system one.
👉 Do you want me to walk you through editing the service in nano right now with the exact copy-paste, so you don’t miss a line?
You said:
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
source venv/bin/activate
pip install gunicorn
Requirement already satisfied: gunicorn in ./venv/lib/python3.12/site-packages (21.2.0)
Requirement already satisfied: packaging in ./venv/lib/python3.12/site-packages (from gunicorn) (25.0)
(venv) michal@quantworld:~/NanoTrace$ sudo nano /etc/systemd/system/nanotrace.service
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl daemon-reexec
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl start nanotrace
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl status nanotrace
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:06:24 UTC; 12s ago
Main PID: 6159 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 14.5M (peak: 139.4M)
CPU: 1.432s
CGroup: /system.slice/nanotrace.service
└─6159 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bi>
Aug 26 22:06:25 quantworld gunicorn[6159]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/>
Aug 26 22:06:25 quantworld gunicorn[6159]: time.sleep(0.1)
Aug 26 22:06:25 quantworld gunicorn[6159]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/>
Aug 26 22:06:25 quantworld gunicorn[6159]: self.reap_workers()
Aug 26 22:06:25 quantworld gunicorn[6159]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/>
Aug 26 22:06:25 quantworld gunicorn[6159]: self.log.error(msg)
Aug 26 22:06:25 quantworld gunicorn[6159]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/>
Aug 26 22:06:25 quantworld gunicorn[6159]: self.error_log.error(msg, *args, **kwargs)
Aug 26 22:06:25 quantworld gunicorn[6159]: Message: 'Worker (pid:6162) was sent SIGTERM!'
Aug 26 22:06:25 quantworld gunicorn[6159]: Arguments: ()
lines 1-20/20 (END)
ChatGPT said:
Nice work, Michal 👍 — systemd is now managing NanoTrace via Gunicorn, and it shows as active (running).
The log lines you see:
vbnetMessage: 'Worker (pid:6162) was sent SIGTERM!'
aren’t fatal — that’s just Gunicorn recycling workers (normal behavior). The important part is:
arduinoActive: active (running)
✅ That means your backend is now running on 127.0.0.1:8000 under systemd.
🔎 Next Step: Put NGINX in Front
Right now, Gunicorn is only reachable inside the VPS (localhost:8000). To make it public at https://nanotrace.org, we need NGINX to forward requests.
1. Create NGINX config
bashsudo nano /etc/nginx/sites-available/nanotrace.conf
Paste this:
nginxserver {
listen 80;
server_name nanotrace.org www.nanotrace.org;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
2. Enable config
bashsudo ln -s /etc/nginx/sites-available/nanotrace.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
3. Add SSL (Let’s Encrypt)
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
After that, you can open:
arduinohttps://nanotrace.org
and see your app (Login/Register page).
👉 Do you want me to also give you the UFW firewall rules to only allow 22, 80, 443 and block everything else, so the VPS is locked down?
You said:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.5-February-24-2025.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: n
Account registered.
Requesting a certificate for nanotrace.org and www.nanotrace.org
Certbot failed to authenticate some domains (authenticator: nginx). The Certificate Authority reported these problems:
Domain: nanotrace.org
Type: connection
Detail: 217.154.56.38: Fetching http://nanotrace.org/.well-known/acme-challenge/CoOOhTDL0cwrgjq1bkfPTB1gu0usN9B-cdI8WGO05m0: Timeout during connect (likely firewall problem)
Domain: www.nanotrace.org
Type: connection
Detail: 217.154.56.38: Fetching http://www.nanotrace.org/.well-known/acme-challenge/nPgLVfZNY42e1W9c717GvbAB6rjXXtTnPmINSDGPHdQ: Timeout during connect (likely firewall problem)
Hint: The Certificate Authority failed to verify the temporary nginx configuration changes made by Certbot. Ensure the listed domains point to this nginx server and that it is accessible from the internet.
Some challenges have failed.
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Thanks for showing me the full error, Michal — this is a classic Let’s Encrypt validation issue.
🔎 What’s happening
-
Certbot asks Let’s Encrypt servers to fetch a file from:
arduinohttp://nanotrace.org/.well-known/acme-challenge/... -
Let’s Encrypt tries to connect to your VPS on port 80 (HTTP).
-
It times out → “likely firewall problem”.
So the issue is not with Flask or Gunicorn — it’s network/domain level.
✅ Fix Checklist
-
Domain DNS must point to your VPS IP
Check that bothnanotrace.organdwww.nanotrace.orgresolve to217.154.56.38:bashdig +short nanotrace.org dig +short www.nanotrace.orgBoth should return
217.154.56.38. -
Port 80 must be open
Sinceufwis inactive, it’s not blocking.
But your hosting provider or Plesk may block direct port 80 unless NGINX is listening.
Run:bashsudo ss -tlnp | grep 80You should see NGINX listening on
0.0.0.0:80. -
Temporary test
Try opening from your PC:arduinohttp://nanotrace.orgYou should see your app (through NGINX proxy).
If it times out, port 80 is blocked upstream.
🚀 Next Steps
-
If DNS is wrong → fix A records in your domain registrar.
-
If DNS is correct but port 80 is blocked → we may need to configure Plesk or open the port at provider firewall.
-
Once
http://nanotrace.orgis reachable, rerun:bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
👉 Do you want me to guide you in checking DNS A records for nanotrace.org first, so we know if the domain points correctly to your VPS?
You said:
cert 217.154.34.203
verify 217.154.34.203
www 217.154.34.203
* 217.154.34.203
chain 217.154.34.203
api 217.154.34.203
admin 217.154.34.203
register 217.154.34.203 just set up DNS but pending
ChatGPT said:
Got it 👍 — thanks for showing me your DNS zone. That explains the Certbot failure:
-
You’ve just updated DNS (all subdomains pointing to
217.154.34.203). -
DNS changes can take some time to propagate globally (anywhere from a few minutes up to 24–48 hours, depending on TTL).
-
Until Let’s Encrypt can resolve
nanotrace.org → 217.154.34.203and connect on port 80, certificate issuance will fail.
✅ What to Do
-
Verify DNS propagation
Run:bashdig +short nanotrace.org dig +short www.nanotrace.orgIf both show
217.154.34.203, DNS is resolving.
You can also check with an online tool like https://dnschecker.org. -
Check port 80 is open & NGINX is listening
bashsudo ss -tlnp | grep 80You should see something like:
nginxLISTEN 0 511 0.0.0.0:80 ...Then, from your PC browser, try:
arduinohttp://nanotrace.orgIf it loads your Flask app, you’re good.
-
Re-run Certbot once DNS works
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
⚠️ Until DNS is fully propagated, Certbot will keep failing because Let’s Encrypt servers won’t know where nanotrace.org lives.
👉 Do you want me to give you a simple temporary check page in NGINX (like “NanoTrace online”) so that when DNS propagates you can confirm port 80 works before retrying Certbot?
You said:
dig +short www.nanotrace.org
217.154.34.203
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfect — that shows DNS for www.nanotrace.org has already propagated to your VPS IP ✅.
Now the only blocker is port 80 reachability. Let’s double-check that NGINX is listening correctly:
1. Check NGINX listening on port 80
Run:
bashsudo ss -tlnp | grep 80
You should see something like:
nginxLISTEN 0 511 0.0.0.0:80 ...
2. Test from VPS itself
Run:
bashcurl -I http://nanotrace.org
Expected:
arduinoHTTP/1.1 200 OK
Server: nginx/...
3. If both are OK
Re-run:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
That should succeed now since DNS is pointing correctly and Let’s Encrypt can fetch the .well-known/acme-challenge.
👉 Want me to also prepare a minimal temporary index page in /var/www/html/index.html (like “NanoTrace Online”) so you instantly know NGINX is serving traffic before retrying Certbot?
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo ss -tlnp | grep 80
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=6268,fd=5),("nginx",pid=6267,fd=5),("nginx",pid=6266,fd=5),("nginx",pid=6265,fd=5),("nginx",pid=6264,fd=5),("nginx",pid=6263,fd=5),("nginx",pid=6262,fd=5),("nginx",pid=6261,fd=5),("nginx",pid=761,fd=5))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=6268,fd=6),("nginx",pid=6267,fd=6),("nginx",pid=6266,fd=6),("nginx",pid=6265,fd=6),("nginx",pid=6264,fd=6),("nginx",pid=6263,fd=6),("nginx",pid=6262,fd=6),("nginx",pid=6261,fd=6),("nginx",pid=761,fd=6))
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
You’re very close. NGINX is listening on :80, so the last mile is proving the vhost works and then issuing the cert.
Step 1 — Verify the nanotrace vhost is serving your app (VPS terminal)
bash# sanity check nginx config & reload
sudo nginx -t && sudo systemctl reload nginx
# ensure your site is enabled
ls -l /etc/nginx/sites-enabled/
# hit the vhost locally using the Host header
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
# expect: HTTP/1.1 200 OK (or 301 if you added redirects)
# optional: confirm proxy to gunicorn works (should return HTML)
curl -H "Host: nanotrace.org" http://127.0.0.1/ | head -n 5
Now, from your local computer, open http://nanotrace.org.
If it still times out, watch NGINX logs while you refresh in your browser:
bashsudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log
(If nothing appears, traffic isn’t reaching the box—double-check your domain registrar’s A records point to 217.154.34.203 everywhere. Your earlier certbot error showed 217.154.56.38, which suggests DNS just changed—some validators may still be hitting the old IP.)
Step 2 — Issue the certificate with Certbot (VPS terminal)
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
Success signs:
-
Certbot edits nginx, obtains cert, and reloads nginx.
-
You can load https://nanotrace.org.
If it still reports a connection timeout, it’s almost always DNS propagation to the old IP. Two quick workarounds:
-
Try again in a bit after confirming
dig +short nanotrace.orgreturns 217.154.34.203 from multiple resolvers. -
Or use the DNS challenge path (no port 80 dependency):
bashsudo certbot -d nanotrace.org -d www.nanotrace.org --manual --preferred-challenges dns certonly(You’ll add TXT records in your DNS; I can walk you through that if needed.)
Ping me with the outputs of:
-
curl -I -H "Host: nanotrace.org" http://127.0.0.1/ -
Any new Certbot error text
…and we’ll finish the lock-in.
You said:
(venv) michal@quantworld:~/NanoTrace$ # sanity check nginx config & reload
sudo nginx -t && sudo systemctl reload nginx
# ensure your site is enabled
ls -l /etc/nginx/sites-enabled/
# hit the vhost locally using the Host header
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
# expect: HTTP/1.1 200 OK (or 301 if you added redirects)
# optional: confirm proxy to gunicorn works (should return HTML)
curl -H "Host: nanotrace.org" http://127.0.0.1/ | head -n 5
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
total 0
lrwxrwxrwx 1 root root 34 Aug 26 20:01 default -> /etc/nginx/sites-available/default
lrwxrwxrwx 1 root root 41 Aug 26 22:08 nanotrace.conf -> /etc/nginx/sites-available/nanotrace.conf
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:20:52 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 166 100 166 0 0 134k 0 --:--:-- --:--:-- --:--:-- 162k
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
You’re super close, Michal. A 502 means NGINX can’t reach Gunicorn on 127.0.0.1:8000. Let’s fix it in two tight steps.
Step 1 — Confirm Gunicorn is listening + read service logs (terminal: VPS)
bashsudo ss -tlnp | grep 8000 sudo systemctl status nanotrace --no-pager sudo journalctl -u nanotrace -n 100 --no-pager
-
If you don’t see
LISTEN ... 127.0.0.1:8000, Gunicorn isn’t actually serving. -
The
journalctloutput will show why (import error, missing env, etc.). Paste the last 10–20 lines to me if it’s unclear.
Step 2 — Run Gunicorn in the foreground to catch errors (same terminal)
Stop the service first, then run it manually:
bashsudo systemctl stop nanotrace
cd ~/NanoTrace
source venv/bin/activate
gunicorn --bind 127.0.0.1:8000 --workers 3 --access-logfile - --error-logfile - backend.app:app
-
If it prints errors, we’ll fix them right away (it’ll tell us exactly what’s wrong).
-
If it starts cleanly and shows requests in the console, open a new VPS tab and try:
bashcurl -I -H "Host: nanotrace.org" http://127.0.0.1/You should get
200 OK. ThenCtrl+Cto stop Gunicorn, and restart the service:bashsudo systemctl start nanotrace sudo systemctl status nanotrace --no-pager
If you hit any error text in Step 2, paste it back here — we’ll squash it fast.
You said:
(venv) michal@quantworld:~/NanoTrace$ # sanity check nginx config & reload
sudo nginx -t && sudo systemctl reload nginx
# ensure your site is enabled
ls -l /etc/nginx/sites-enabled/
# hit the vhost locally using the Host header
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
# expect: HTTP/1.1 200 OK (or 301 if you added redirects)
# optional: confirm proxy to gunicorn works (should return HTML)
curl -H "Host: nanotrace.org" http://127.0.0.1/ | head -n 5
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
total 0
lrwxrwxrwx 1 root root 34 Aug 26 20:01 default -> /etc/nginx/sites-available/default
lrwxrwxrwx 1 root root 41 Aug 26 22:08 nanotrace.conf -> /etc/nginx/sites-available/nanotrace.conf
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:20:52 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 166 100 166 0 0 134k 0 --:--:-- --:--:-- --:--:-- 162k
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
(venv) michal@quantworld:~/NanoTrace$ sudo ss -tlnp | grep 8000
sudo systemctl status nanotrace --no-pager
sudo journalctl -u nanotrace -n 100 --no-pager
× nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: failed (Result: exit-code) since Tue 2025-08-26 22:07:00 UTC; 16min ago
Duration: 722ms
Process: 6196 ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 backend.app:app (code=exited, status=3)
Main PID: 6196 (code=exited, status=3)
CPU: 1.444s
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 6.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:07:00 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Consumed 1.444s CPU time.
Aug 26 22:07:00 quantworld gunicorn[6197]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/wsgiapp.py", line 48, in load_wsgiapp
Aug 26 22:07:00 quantworld gunicorn[6197]: return util.import_app(self.app_uri)
Aug 26 22:07:00 quantworld gunicorn[6197]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6197]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/util.py", line 371, in import_app
Aug 26 22:07:00 quantworld gunicorn[6197]: mod = importlib.import_module(module)
Aug 26 22:07:00 quantworld gunicorn[6197]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6197]: File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
Aug 26 22:07:00 quantworld gunicorn[6197]: return _bootstrap._gcd_import(name[level:], package, level)
Aug 26 22:07:00 quantworld gunicorn[6197]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6197]: File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
Aug 26 22:07:00 quantworld gunicorn[6197]: File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
Aug 26 22:07:00 quantworld gunicorn[6197]: File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
Aug 26 22:07:00 quantworld gunicorn[6197]: File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
Aug 26 22:07:00 quantworld gunicorn[6197]: File "<frozen importlib._bootstrap_external>", line 995, in exec_module
Aug 26 22:07:00 quantworld gunicorn[6197]: File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
Aug 26 22:07:00 quantworld gunicorn[6197]: File "/home/michal/NanoTrace/backend/app/__init__.py", line 6, in <module>
Aug 26 22:07:00 quantworld gunicorn[6197]: from config.config import Config
Aug 26 22:07:00 quantworld gunicorn[6197]: ModuleNotFoundError: No module named 'config'
Aug 26 22:07:00 quantworld gunicorn[6197]: [2025-08-26 22:07:00 +0000] [6197] [INFO] Worker exiting (pid: 6197)
Aug 26 22:07:00 quantworld gunicorn[6198]: [2025-08-26 22:07:00 +0000] [6198] [ERROR] Exception in worker process
Aug 26 22:07:00 quantworld gunicorn[6198]: Traceback (most recent call last):
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 609, in spawn_worker
Aug 26 22:07:00 quantworld gunicorn[6198]: worker.init_process()
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/workers/base.py", line 134, in init_process
Aug 26 22:07:00 quantworld gunicorn[6198]: self.load_wsgi()
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/workers/base.py", line 146, in load_wsgi
Aug 26 22:07:00 quantworld gunicorn[6198]: self.wsgi = self.app.wsgi()
Aug 26 22:07:00 quantworld gunicorn[6198]: ^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/base.py", line 67, in wsgi
Aug 26 22:07:00 quantworld gunicorn[6198]: self.callable = self.load()
Aug 26 22:07:00 quantworld gunicorn[6198]: ^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/wsgiapp.py", line 58, in load
Aug 26 22:07:00 quantworld gunicorn[6198]: return self.load_wsgiapp()
Aug 26 22:07:00 quantworld gunicorn[6198]: ^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/wsgiapp.py", line 48, in load_wsgiapp
Aug 26 22:07:00 quantworld gunicorn[6198]: return util.import_app(self.app_uri)
Aug 26 22:07:00 quantworld gunicorn[6198]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/util.py", line 371, in import_app
Aug 26 22:07:00 quantworld gunicorn[6198]: mod = importlib.import_module(module)
Aug 26 22:07:00 quantworld gunicorn[6198]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
Aug 26 22:07:00 quantworld gunicorn[6198]: return _bootstrap._gcd_import(name[level:], package, level)
Aug 26 22:07:00 quantworld gunicorn[6198]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6198]: File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
Aug 26 22:07:00 quantworld gunicorn[6198]: File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
Aug 26 22:07:00 quantworld gunicorn[6198]: File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
Aug 26 22:07:00 quantworld gunicorn[6198]: File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
Aug 26 22:07:00 quantworld gunicorn[6198]: File "<frozen importlib._bootstrap_external>", line 995, in exec_module
Aug 26 22:07:00 quantworld gunicorn[6198]: File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
Aug 26 22:07:00 quantworld gunicorn[6198]: File "/home/michal/NanoTrace/backend/app/__init__.py", line 6, in <module>
Aug 26 22:07:00 quantworld gunicorn[6198]: from config.config import Config
Aug 26 22:07:00 quantworld gunicorn[6198]: ModuleNotFoundError: No module named 'config'
Aug 26 22:07:00 quantworld gunicorn[6198]: [2025-08-26 22:07:00 +0000] [6198] [INFO] Worker exiting (pid: 6198)
Aug 26 22:07:00 quantworld gunicorn[6199]: [2025-08-26 22:07:00 +0000] [6199] [ERROR] Exception in worker process
Aug 26 22:07:00 quantworld gunicorn[6199]: Traceback (most recent call last):
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 609, in spawn_worker
Aug 26 22:07:00 quantworld gunicorn[6199]: worker.init_process()
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/workers/base.py", line 134, in init_process
Aug 26 22:07:00 quantworld gunicorn[6199]: self.load_wsgi()
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/workers/base.py", line 146, in load_wsgi
Aug 26 22:07:00 quantworld gunicorn[6199]: self.wsgi = self.app.wsgi()
Aug 26 22:07:00 quantworld gunicorn[6199]: ^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/base.py", line 67, in wsgi
Aug 26 22:07:00 quantworld gunicorn[6199]: self.callable = self.load()
Aug 26 22:07:00 quantworld gunicorn[6199]: ^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/wsgiapp.py", line 58, in load
Aug 26 22:07:00 quantworld gunicorn[6199]: return self.load_wsgiapp()
Aug 26 22:07:00 quantworld gunicorn[6199]: ^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/wsgiapp.py", line 48, in load_wsgiapp
Aug 26 22:07:00 quantworld gunicorn[6199]: return util.import_app(self.app_uri)
Aug 26 22:07:00 quantworld gunicorn[6199]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/util.py", line 371, in import_app
Aug 26 22:07:00 quantworld gunicorn[6199]: mod = importlib.import_module(module)
Aug 26 22:07:00 quantworld gunicorn[6199]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
Aug 26 22:07:00 quantworld gunicorn[6199]: return _bootstrap._gcd_import(name[level:], package, level)
Aug 26 22:07:00 quantworld gunicorn[6199]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Aug 26 22:07:00 quantworld gunicorn[6199]: File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
Aug 26 22:07:00 quantworld gunicorn[6199]: File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
Aug 26 22:07:00 quantworld gunicorn[6199]: File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
Aug 26 22:07:00 quantworld gunicorn[6199]: File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
Aug 26 22:07:00 quantworld gunicorn[6199]: File "<frozen importlib._bootstrap_external>", line 995, in exec_module
Aug 26 22:07:00 quantworld gunicorn[6199]: File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
Aug 26 22:07:00 quantworld gunicorn[6199]: File "/home/michal/NanoTrace/backend/app/__init__.py", line 6, in <module>
Aug 26 22:07:00 quantworld gunicorn[6199]: from config.config import Config
Aug 26 22:07:00 quantworld gunicorn[6199]: ModuleNotFoundError: No module named 'config'
Aug 26 22:07:00 quantworld gunicorn[6199]: [2025-08-26 22:07:00 +0000] [6199] [INFO] Worker exiting (pid: 6199)
Aug 26 22:07:00 quantworld gunicorn[6196]: [2025-08-26 22:07:00 +0000] [6196] [ERROR] Worker (pid:6197) exited with code 3
Aug 26 22:07:00 quantworld gunicorn[6196]: [2025-08-26 22:07:00 +0000] [6196] [ERROR] Worker (pid:6198) was sent SIGTERM!
Aug 26 22:07:00 quantworld gunicorn[6196]: [2025-08-26 22:07:00 +0000] [6196] [ERROR] Worker (pid:6199) was sent SIGTERM!
Aug 26 22:07:00 quantworld gunicorn[6196]: [2025-08-26 22:07:00 +0000] [6196] [ERROR] Shutting down: Master
Aug 26 22:07:00 quantworld gunicorn[6196]: [2025-08-26 22:07:00 +0000] [6196] [ERROR] Reason: Worker failed to boot.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=3/NOTIMPLEMENTED
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Consumed 1.444s CPU time.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 6.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:07:00 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:07:00 quantworld systemd[1]: nanotrace.service: Consumed 1.444s CPU time.
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Great debugging, Michal — the logs tell us exactly what’s wrong:
ModuleNotFoundError: No module named 'config'
Gunicorn is importing backend.app, which imports from config.config import Config. In service mode, Python can’t find a top-level config package. We’ll fix it cleanly.
Step 1 — Fix the import (VPS terminal)
bash# make backend/config a proper package
touch ~/NanoTrace/backend/config/__init__.py
# update the import to an absolute package path
sed -i 's/from config.config import Config/from backend.config.config import Config/' \
~/NanoTrace/backend/app/__init__.py
Step 2 — Restart the service and test (same terminal)
bashsudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
sudo ss -tlnp | grep 8000 # expect LISTEN on 127.0.0.1:8000
# hit through nginx using Host header
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
-
If you get
HTTP/1.1 200 OK(or your app HTML on the non--Iversion), you’re golden. -
Then you can run SSL issuance next:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
Send me the status and curl results, and we’ll take the next move.
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
sudo ss -tlnp | grep 8000 # expect LISTEN on 127.0.0.1:8000
# hit through nginx using Host header
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:25:41 UTC; 17ms ago
Main PID: 6491 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 3.6M (peak: 3.6M)
CPU: 16ms
CGroup: /system.slice/nanotrace.service
└─6491 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bi…
Aug 26 22:25:41 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:25:41 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
ChatGPT said:
Good news: the 502 is just because Gunicorn can’t import your app correctly under systemd. Two quick fixes and we’re live.
Step 1 — Fix imports for service mode (VPS terminal)
bash# 1) Ensure backend is a package
touch ~/NanoTrace/backend/__init__.py
# 2) Ensure backend/config is a package (you already did this, harmless to re-run)
touch ~/NanoTrace/backend/config/__init__.py
# 3) Fix the import inside backend/app.py
sed -i 's/^from app import create_app$/from backend.app import create_app/' ~/NanoTrace/backend/app.py
Step 2 — Restart service and verify (VPS terminal)
bashsudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
# Confirm Gunicorn is listening
sudo ss -tlnp | grep 8000
# Hit through NGINX using Host header (proves end-to-end)
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
# If you want to see HTML:
curl -H "Host: nanotrace.org" http://127.0.0.1/ | head -n 10
What you should see
-
status= active (running) -
ssshowsLISTEN ... 127.0.0.1:8000(gunicorn) -
curl -IreturnsHTTP/1.1 200 OK(or 301 if you later force HTTPS)
If any command still misbehaves, paste the last 20 lines of:
bashsudo journalctl -u nanotrace -n 50 --no-pager
Once curl -I -H "Host: nanotrace.org" http://127.0.0.1/ is 200 OK, your site should load at:
arduinohttp://nanotrace.org
Then you can issue SSL:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
You’ve got this 💪
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
# Confirm Gunicorn is listening
sudo ss -tlnp | grep 8000
# Hit through NGINX using Host header (proves end-to-end)
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
# If you want to see HTML:
curl -H "Host: nanotrace.org" http://127.0.0.1/ | head -n 10
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:30:45 UTC; 19ms ago
Main PID: 6677 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 4.0M (peak: 4.0M)
CPU: 16ms
CGroup: /system.slice/nanotrace.service
└─6677 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bi…
Aug 26 22:30:45 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:30:45 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 166 100 166 0 0 174k 0 --:--:-- --:--:-- --:--:-- 162k
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
You’re super close. Let’s fix the 502 with two precise steps.
Step 1 — Verify Gunicorn is actually serving on :8000 and see errors
Run these three checks (same terminal on the VPS):
bash# A) Is anything listening on 127.0.0.1:8000?
sudo ss -tlnp | grep 8000 || echo "nothing on :8000"
# B) Can we reach Gunicorn directly (bypasses NGINX)?
curl -I http://127.0.0.1:8000 || echo "curl to :8000 failed"
# C) Show recent service logs to catch import/traceback
sudo journalctl -u nanotrace -n 100 --no-pager
If A or B fail, Gunicorn isn’t binding or workers are dying; C will show the reason.
Step 2 — Make imports rock-solid for service mode, add logs, restart
Edit the service to set PYTHONPATH, force cwd, and crank up logging:
bashsudo nano /etc/systemd/system/nanotrace.service
Replace the middle with this:
ini[Service]
User=michal
Group=www-data
WorkingDirectory=/home/michal/NanoTrace
Environment="PATH=/home/michal/NanoTrace/venv/bin"
Environment="PYTHONPATH=/home/michal/NanoTrace"
ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn \
--chdir /home/michal/NanoTrace \
--workers 3 \
--bind 127.0.0.1:8000 \
--log-level debug \
--access-logfile /var/log/nanotrace_access.log \
--error-logfile /var/log/nanotrace_error.log \
backend.app:app
Restart=always
Apply and test:
bashsudo systemctl daemon-reexec
sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
sudo ss -tlnp | grep 8000
curl -I http://127.0.0.1:8000
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
If curl -I http://127.0.0.1:8000 returns 200 OK, NGINX will stop 502’ing and http://nanotrace.org should load. If anything still fails, paste the last 30 lines of:
bashsudo tail -n 30 /var/log/nanotrace_error.log
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
# Confirm Gunicorn is listening
sudo ss -tlnp | grep 8000
# Hit through NGINX using Host header (proves end-to-end)
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
# If you want to see HTML:
curl -H "Host: nanotrace.org" http://127.0.0.1/ | head -n 10
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:30:45 UTC; 19ms ago
Main PID: 6677 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 4.0M (peak: 4.0M)
CPU: 16ms
CGroup: /system.slice/nanotrace.service
└─6677 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bi…
Aug 26 22:30:45 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:30:45 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 166 100 166 0 0 174k 0 --:--:-- --:--:-- --:--:-- 162k
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
(venv) michal@quantworld:~/NanoTrace$ # A) Is anything listening on 127.0.0.1:8000?
sudo ss -tlnp | grep 8000 || echo "nothing on :8000"
# B) Can we reach Gunicorn directly (bypasses NGINX)?
curl -I http://127.0.0.1:8000 || echo "curl to :8000 failed"
# C) Show recent service logs to catch import/traceback
sudo journalctl -u nanotrace -n 100 --no-pager
nothing on :8000
curl: (7) Failed to connect to 127.0.0.1 port 8000 after 0 ms: Couldn't connect to server
curl to :8000 failed
Aug 26 22:30:48 quantworld gunicorn[6697]: sys.exit(run())
Aug 26 22:30:48 quantworld gunicorn[6697]: ^^^^^
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/wsgiapp.py", line 67, in run
Aug 26 22:30:48 quantworld gunicorn[6697]: WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]").run()
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/base.py", line 236, in run
Aug 26 22:30:48 quantworld gunicorn[6697]: super().run()
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/base.py", line 72, in run
Aug 26 22:30:48 quantworld gunicorn[6697]: Arbiter(self).run()
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 229, in run
Aug 26 22:30:48 quantworld gunicorn[6697]: self.halt(reason=inst.reason, exit_status=inst.exit_status)
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 342, in halt
Aug 26 22:30:48 quantworld gunicorn[6697]: self.stop()
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 396, in stop
Aug 26 22:30:48 quantworld gunicorn[6697]: time.sleep(0.1)
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 242, in handle_chld
Aug 26 22:30:48 quantworld gunicorn[6697]: self.reap_workers()
Aug 26 22:30:48 quantworld gunicorn[6697]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 533, in reap_workers
Aug 26 22:30:48 quantworld gunicorn[6697]: raise HaltServer(reason, self.APP_LOAD_ERROR)
Aug 26 22:30:48 quantworld gunicorn[6697]: gunicorn.errors.HaltServer: <HaltServer 'App failed to load.' 4>
Aug 26 22:30:48 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:30:48 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:30:48 quantworld systemd[1]: nanotrace.service: Consumed 1.489s CPU time.
Aug 26 22:30:48 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 3.
Aug 26 22:30:48 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:30:48 quantworld gunicorn[6702]: [2025-08-26 22:30:48 +0000] [6702] [INFO] Starting gunicorn 21.2.0
Aug 26 22:30:48 quantworld gunicorn[6702]: [2025-08-26 22:30:48 +0000] [6702] [INFO] Listening at: http://127.0.0.1:8000 (6702)
Aug 26 22:30:48 quantworld gunicorn[6702]: [2025-08-26 22:30:48 +0000] [6702] [INFO] Using worker: sync
Aug 26 22:30:48 quantworld gunicorn[6703]: [2025-08-26 22:30:48 +0000] [6703] [INFO] Booting worker with pid: 6703
Aug 26 22:30:48 quantworld gunicorn[6704]: [2025-08-26 22:30:48 +0000] [6704] [INFO] Booting worker with pid: 6704
Aug 26 22:30:48 quantworld gunicorn[6705]: [2025-08-26 22:30:48 +0000] [6705] [INFO] Booting worker with pid: 6705
Aug 26 22:30:49 quantworld gunicorn[6703]: Failed to find attribute 'app' in 'backend.app'.
Aug 26 22:30:49 quantworld gunicorn[6703]: [2025-08-26 22:30:49 +0000] [6703] [INFO] Worker exiting (pid: 6703)
Aug 26 22:30:49 quantworld gunicorn[6704]: Failed to find attribute 'app' in 'backend.app'.
Aug 26 22:30:49 quantworld gunicorn[6704]: [2025-08-26 22:30:49 +0000] [6704] [INFO] Worker exiting (pid: 6704)
Aug 26 22:30:49 quantworld gunicorn[6705]: Failed to find attribute 'app' in 'backend.app'.
Aug 26 22:30:49 quantworld gunicorn[6705]: [2025-08-26 22:30:49 +0000] [6705] [INFO] Worker exiting (pid: 6705)
Aug 26 22:30:49 quantworld gunicorn[6702]: [2025-08-26 22:30:49 +0000] [6702] [ERROR] Worker (pid:6703) exited with code 4
Aug 26 22:30:49 quantworld gunicorn[6702]: [2025-08-26 22:30:49 +0000] [6702] [ERROR] Worker (pid:6704) was sent SIGTERM!
Aug 26 22:30:49 quantworld gunicorn[6702]: [2025-08-26 22:30:49 +0000] [6702] [ERROR] Worker (pid:6705) was sent SIGTERM!
Aug 26 22:30:49 quantworld gunicorn[6702]: [2025-08-26 22:30:49 +0000] [6702] [ERROR] Shutting down: Master
Aug 26 22:30:49 quantworld gunicorn[6702]: [2025-08-26 22:30:49 +0000] [6702] [ERROR] Reason: App failed to load.
Aug 26 22:30:49 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=4/NOPERMISSION
Aug 26 22:30:49 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:30:49 quantworld systemd[1]: nanotrace.service: Consumed 1.555s CPU time.
Aug 26 22:30:49 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 4.
Aug 26 22:30:49 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:30:49 quantworld gunicorn[6707]: [2025-08-26 22:30:49 +0000] [6707] [INFO] Starting gunicorn 21.2.0
Aug 26 22:30:49 quantworld gunicorn[6707]: [2025-08-26 22:30:49 +0000] [6707] [INFO] Listening at: http://127.0.0.1:8000 (6707)
Aug 26 22:30:49 quantworld gunicorn[6707]: [2025-08-26 22:30:49 +0000] [6707] [INFO] Using worker: sync
Aug 26 22:30:49 quantworld gunicorn[6708]: [2025-08-26 22:30:49 +0000] [6708] [INFO] Booting worker with pid: 6708
Aug 26 22:30:49 quantworld gunicorn[6709]: [2025-08-26 22:30:49 +0000] [6709] [INFO] Booting worker with pid: 6709
Aug 26 22:30:49 quantworld gunicorn[6710]: [2025-08-26 22:30:49 +0000] [6710] [INFO] Booting worker with pid: 6710
Aug 26 22:30:50 quantworld gunicorn[6709]: Failed to find attribute 'app' in 'backend.app'.
Aug 26 22:30:50 quantworld gunicorn[6709]: [2025-08-26 22:30:50 +0000] [6709] [INFO] Worker exiting (pid: 6709)
Aug 26 22:30:50 quantworld gunicorn[6708]: Failed to find attribute 'app' in 'backend.app'.
Aug 26 22:30:50 quantworld gunicorn[6708]: [2025-08-26 22:30:50 +0000] [6708] [INFO] Worker exiting (pid: 6708)
Aug 26 22:30:50 quantworld gunicorn[6710]: Failed to find attribute 'app' in 'backend.app'.
Aug 26 22:30:50 quantworld gunicorn[6710]: [2025-08-26 22:30:50 +0000] [6710] [INFO] Worker exiting (pid: 6710)
Aug 26 22:30:50 quantworld gunicorn[6707]: [2025-08-26 22:30:50 +0000] [6707] [ERROR] Worker (pid:6709) exited with code 4
Aug 26 22:30:50 quantworld gunicorn[6707]: [2025-08-26 22:30:50 +0000] [6707] [ERROR] Worker (pid:6710) exited with code 4
Aug 26 22:30:50 quantworld gunicorn[6707]: Traceback (most recent call last):
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 209, in run
Aug 26 22:30:50 quantworld gunicorn[6707]: self.sleep()
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 360, in sleep
Aug 26 22:30:50 quantworld gunicorn[6707]: [2025-08-26 22:30:50 +0000] [6707] [ERROR] Worker (pid:6708) exited with code 4
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 242, in handle_chld
Aug 26 22:30:50 quantworld gunicorn[6707]: self.reap_workers()
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 533, in reap_workers
Aug 26 22:30:50 quantworld gunicorn[6707]: raise HaltServer(reason, self.APP_LOAD_ERROR)
Aug 26 22:30:50 quantworld gunicorn[6707]: gunicorn.errors.HaltServer: <HaltServer 'App failed to load.' 4>
Aug 26 22:30:50 quantworld gunicorn[6707]: During handling of the above exception, another exception occurred:
Aug 26 22:30:50 quantworld gunicorn[6707]: Traceback (most recent call last):
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/bin/gunicorn", line 8, in <module>
Aug 26 22:30:50 quantworld gunicorn[6707]: sys.exit(run())
Aug 26 22:30:50 quantworld gunicorn[6707]: ^^^^^
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/wsgiapp.py", line 67, in run
Aug 26 22:30:50 quantworld gunicorn[6707]: WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]").run()
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/base.py", line 236, in run
Aug 26 22:30:50 quantworld gunicorn[6707]: super().run()
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/app/base.py", line 72, in run
Aug 26 22:30:50 quantworld gunicorn[6707]: Arbiter(self).run()
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 229, in run
Aug 26 22:30:50 quantworld gunicorn[6707]: self.halt(reason=inst.reason, exit_status=inst.exit_status)
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 342, in halt
Aug 26 22:30:50 quantworld gunicorn[6707]: self.stop()
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 396, in stop
Aug 26 22:30:50 quantworld gunicorn[6707]: time.sleep(0.1)
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 242, in handle_chld
Aug 26 22:30:50 quantworld gunicorn[6707]: self.reap_workers()
Aug 26 22:30:50 quantworld gunicorn[6707]: File "/home/michal/NanoTrace/venv/lib/python3.12/site-packages/gunicorn/arbiter.py", line 533, in reap_workers
Aug 26 22:30:50 quantworld gunicorn[6707]: raise HaltServer(reason, self.APP_LOAD_ERROR)
Aug 26 22:30:50 quantworld gunicorn[6707]: gunicorn.errors.HaltServer: <HaltServer 'App failed to load.' 4>
Aug 26 22:30:50 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:30:50 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:30:50 quantworld systemd[1]: nanotrace.service: Consumed 1.697s CPU time.
Aug 26 22:30:50 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 5.
Aug 26 22:30:50 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:30:50 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:30:50 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:30:50 quantworld systemd[1]: nanotrace.service: Consumed 1.697s CPU time.
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
You're right on the edge. Let’s fix the Gunicorn target cleanly so NGINX stops 502’ing.
Step 1 — Create a dedicated WSGI entrypoint
Terminal: VPS
bash# 1) create a clean wsgi module that exposes `app`
nano ~/NanoTrace/backend/wsgi.py
Paste:
pythonfrom backend.app import create_app # this imports create_app from backend/app/__init__.py
app = create_app()
Save & exit.
Step 2 — Point systemd to the WSGI module and restart
Terminal: VPS
bash# 2) edit service to use backend.wsgi:app
sudo nano /etc/systemd/system/nanotrace.service
Replace the ExecStart block so it looks like this (keep the rest as-is):
ini[Service]
User=michal
Group=www-data
WorkingDirectory=/home/michal/NanoTrace
Environment="PATH=/home/michal/NanoTrace/venv/bin"
Environment="PYTHONPATH=/home/michal/NanoTrace"
ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn \
--chdir /home/michal/NanoTrace \
--workers 3 \
--bind 127.0.0.1:8000 \
--log-level info \
--access-logfile /var/log/nanotrace_access.log \
--error-logfile /var/log/nanotrace_error.log \
backend.wsgi:app
Restart=always
Apply & test:
bashsudo systemctl daemon-reexec
sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
sudo ss -tlnp | grep 8000 # should show LISTEN on 127.0.0.1:8000
curl -I http://127.0.0.1:8000 # expect HTTP/1.1 200 OK
curl -I -H "Host: nanotrace.org" http://127.0.0.1/ # through NGINX, expect 200 OK
If any command doesn’t match the expected result, paste the last lines of:
bashsudo tail -n 50 /var/log/nanotrace_error.log
You’ve got this — once this returns 200 OK, http://nanotrace.org will load, and then we can issue the SSL cert.
You said:
curl -H "Host: nanotrace.org" http://127.0.0.1/ | head -n 10
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:30:45 UTC; 19ms ago
Main PID: 6677 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 4.0M (peak: 4.0M)
CPU: 16ms
CGroup: /system.slice/nanotrace.service
└─6677 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --workers 3 --bi…
Aug 26 22:30:45 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:30:45 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 166 100 166 0 0 174k 0 --:--:-- --:--:-- --:--:-- 162k
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
^C
(venv) michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/backend/wsgi.py
(venv) michal@quantworld:~/NanoTrace$ # 2) edit service to use backend.wsgi:app
sudo nano /etc/systemd/system/nanotrace.service
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl daemon-reexec
sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
sudo ss -tlnp | grep 8000 # should show LISTEN on 127.0.0.1:8000
curl -I http://127.0.0.1:8000 # expect HTTP/1.1 200 OK
curl -I -H "Host: nanotrace.org" http://127.0.0.1/ # through NGINX, expect 200 OK
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:42:01 UTC; 23ms ago
Main PID: 6864 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 3.4M (peak: 3.4M)
CPU: 17ms
CGroup: /system.slice/nanotrace.service
└─6864 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --chdir /home/mi…
Aug 26 22:42:01 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
curl: (7) Failed to connect to 127.0.0.1 port 8000 after 0 ms: Couldn't connect to server
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:42:01 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
(venv) michal@quantworld:~/NanoTrace$ sudo tail -n 50 /var/log/nanotrace_error.log
tail: cannot open '/var/log/nanotrace_error.log' for reading: No such file or directory
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Nice trace dump — the key line is:
Failed to find attribute
appinbackend.app.
That means Gunicorn can’t import your Flask app because of package import paths. We’ll fix the imports so they’re package-relative and then retest. Two small steps.
Step 1 — Fix package imports (VPS terminal)
Run these edits to make everything importable under the backend package:
bash# 1) ensure packages
touch ~/NanoTrace/backend/__init__.py
touch ~/NanoTrace/backend/config/__init__.py
# 2) __init__.py (Flask factory) — use package-relative imports
# from config.config -> backend.config.config
# from app.views.... -> .views....
sed -i 's/from config.config import Config/from backend.config.config import Config/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.auth/from .views.auth/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.main/from .views.main/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.certificates/from .views.certificates/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.admin/from .views.admin/' \
~/NanoTrace/backend/app/__init__.py
# 3) views — import db/login_manager from the package (.. means "backend.app")
sed -i 's/from app import db/from .. import db/' \
~/NanoTrace/backend/app/views/certificates.py
sed -i 's/from app import db/from .. import db/' \
~/NanoTrace/backend/app/views/admin.py
sed -i 's/from app import db, login_manager/from .. import db, login_manager/' \
~/NanoTrace/backend/app/views/auth.py 2>/dev/null || true
# 4) models — already relative in your setup, but ensure user model uses relative
sed -i 's/from .. import db, login_manager/from .. import db, login_manager/' \
~/NanoTrace/backend/app/models/user.py
# 5) app entrypoints
# backend/app.py should import from backend.app (for the dev run)
sed -i 's/^from app import create_app$/from backend.app import create_app/' \
~/NanoTrace/backend/app.py
If any sed line says “no such file”, that’s ok; we just move on.
Quick sanity check (still on the VPS):
bashcd ~/NanoTrace
source venv/bin/activate
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
You should see WSGI app loaded OK: <Flask 'backend.app'>.
Step 2 — Restart Gunicorn and test
bashsudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
# confirm it's listening
sudo ss -tlnp | grep 8000
# test Gunicorn directly
curl -I http://127.0.0.1:8000
# test through NGINX host routing
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
Expected:
-
ssshowsLISTEN ... 127.0.0.1:8000(gunicorn) -
both
curlcalls returnHTTP/1.1 200 OK
If anything still fails, show me the last lines:
bashsudo journalctl -u nanotrace -n 50 --no-pager
Once we get 200 OK locally, http://nanotrace.org will load from your PC, and we can immediately finish SSL with:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
You said:
Aug 26 22:42:01 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
curl: (7) Failed to connect to 127.0.0.1 port 8000 after 0 ms: Couldn't connect to server
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:42:01 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
(venv) michal@quantworld:~/NanoTrace$ sudo tail -n 50 /var/log/nanotrace_error.log
tail: cannot open '/var/log/nanotrace_error.log' for reading: No such file or directory
(venv) michal@quantworld:~/NanoTrace$ # 1) ensure packages
touch ~/NanoTrace/backend/__init__.py
touch ~/NanoTrace/backend/config/__init__.py
# 2) __init__.py (Flask factory) — use package-relative imports
# from config.config -> backend.config.config
# from app.views.... -> .views....
sed -i 's/from config.config import Config/from backend.config.config import Config/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.auth/from .views.auth/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.main/from .views.main/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.certificates/from .views.certificates/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.admin/from .views.admin/' \
~/NanoTrace/backend/app/__init__.py
# 3) views — import db/login_manager from the package (.. means "backend.app")
sed -i 's/from app import db/from .. import db/' \
~/NanoTrace/backend/app/views/certificates.py
sed -i 's/from app import db/from .. import db/' \
~/NanoTrace/backend/app/views/admin.py
sed -i 's/from app import db, login_manager/from .. import db, login_manager/' \
~/NanoTrace/backend/app/views/auth.py 2>/dev/null || true
# 4) models — already relative in your setup, but ensure user model uses relative
sed -i 's/from .. import db, login_manager/from .. import db, login_manager/' \
~/NanoTrace/backend/app/models/user.py
# 5) app entrypoints
# backend/app.py should import from backend.app (for the dev run)
sed -i 's/^from app import create_app$/from backend.app import create_app/' \
~/NanoTrace/backend/app.py
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
source venv/bin/activate
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/michal/NanoTrace/backend/wsgi.py", line 2, in <module>
app = create_app()
^^^^^^^^^^^^
File "/home/michal/NanoTrace/backend/app/__init__.py", line 25, in create_app
from .views.auth import bp as auth_bp
File "/home/michal/NanoTrace/backend/app/views/auth.py", line 3, in <module>
from app.models.user import User
ModuleNotFoundError: No module named 'app'
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfect, you nailed the test — the error now shows the real blocker:
pgsqlfrom app.models.user import User
ModuleNotFoundError: No module named 'app'
That means inside your backend/app/views/auth.py (and maybe others), it’s still importing app.models... instead of package-relative ..models....
Step 1 — Fix the imports in auth.py
Edit the file:
bashnano ~/NanoTrace/backend/app/views/auth.py
Look for:
pythonfrom app.models.user import User
Change it to:
pythonfrom ..models.user import User
Also check for any other from app... in that file — switch them to relative ...
Step 2 — Re-test import manually
Still inside venv:
bashcd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Expected:
luaWSGI app loaded OK: <Flask 'backend.app'>
Step 3 — Restart Gunicorn + Test
bashsudo systemctl daemon-reexec
sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
sudo ss -tlnp | grep 8000 # should LISTEN on 127.0.0.1:8000
curl -I http://127.0.0.1:8000 # expect HTTP/1.1 200 OK
curl -I -H "Host: nanotrace.org" http://127.0.0.1/ # NGINX proxy test
Do you want me to also give you a one-liner sed fix for all from app.models... across your project, so you don’t have to hunt them manually?
2/2
You said:
# backend/app.py should import from backend.app (for the dev run)
sed -i 's/^from app import create_app$/from backend.app import create_app/' \
~/NanoTrace/backend/app.py
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
source venv/bin/activate
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/michal/NanoTrace/backend/wsgi.py", line 2, in <module>
app = create_app()
^^^^^^^^^^^^
File "/home/michal/NanoTrace/backend/app/__init__.py", line 25, in create_app
from .views.auth import bp as auth_bp
File "/home/michal/NanoTrace/backend/app/views/auth.py", line 3, in <module>
from app.models.user import User
ModuleNotFoundError: No module named 'app'
(venv) michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/backend/app/views/auth.py
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/michal/NanoTrace/backend/wsgi.py", line 2, in <module>
app = create_app()
^^^^^^^^^^^^
File "/home/michal/NanoTrace/backend/app/__init__.py", line 25, in create_app
from .views.auth import bp as auth_bp
File "/home/michal/NanoTrace/backend/app/views/auth.py", line 4, in <module>
from app import db
ModuleNotFoundError: No module named 'app'
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Nice progress — you fixed the first import. Now the error shows the next bad line:
javascriptfrom app import db
ModuleNotFoundError: No module named 'app'
Because your project lives under backend/, all imports inside backend/app/views/... should use relative imports (with dots).
Step 1 — Fix auth.py
Open it again:
bashnano ~/NanoTrace/backend/app/views/auth.py
Find:
pythonfrom app import db
Replace with:
pythonfrom .. import db
Step 2 — Quick check for all similar imports
To avoid chasing these one-by-one, let’s scan all files under backend/app/views and backend/app/models:
bashgrep -R "from app" ~/NanoTrace/backend/app
Every result you see needs to be changed:
-
from app import db→from .. import db -
from app import db, login_manager→from .. import db, login_manager -
from app.models.user import User→from ..models.user import User
Step 3 — Re-test
After saving, run:
bashcd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Expected:
luaWSGI app loaded OK: <Flask 'backend.app'>
Do you want me to give you a sed one-liner to automatically rewrite all from app ... imports into their correct relative form across your project? That way you don’t need to edit each file manually.
You said:
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/michal/NanoTrace/backend/wsgi.py", line 2, in <module>
app = create_app()
^^^^^^^^^^^^
File "/home/michal/NanoTrace/backend/app/__init__.py", line 25, in create_app
from .views.auth import bp as auth_bp
File "/home/michal/NanoTrace/backend/app/views/auth.py", line 4, in <module>
from app import db
ModuleNotFoundError: No module named 'app'
(venv) michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/backend/app/views/auth.py
(venv) michal@quantworld:~/NanoTrace$ grep -R "from app" ~/NanoTrace/backend/app
/home/michal/NanoTrace/backend/app/views/admin.py:from app.models.certificate import Certificate
/home/michal/NanoTrace/backend/app/views/certificates.py:from app.models.certificate import Certificate
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfect, now we see the remaining offenders:
pgsqlbackend/app/views/admin.py:from app.models.certificate import Certificate
backend/app/views/certificates.py:from app.models.certificate import Certificate
These should also be relative imports, because your package root is backend.app.
Step 1 — Fix admin.py
Open it:
bashnano ~/NanoTrace/backend/app/views/admin.py
Change:
pythonfrom app.models.certificate import Certificate
to:
pythonfrom ..models.certificate import Certificate
Step 2 — Fix certificates.py
Open:
bashnano ~/NanoTrace/backend/app/views/certificates.py
Change:
pythonfrom app.models.certificate import Certificate
to:
pythonfrom ..models.certificate import Certificate
Step 3 — Re-test
Run again:
bashcd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Expected output:
luaWSGI app loaded OK: <Flask 'backend.app'>
Do you want me to also give you a one-liner sed command to rewrite all from app... to from .. so you don’t have to open files manually again?
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl daemon-reexec
sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
sudo ss -tlnp | grep 8000 # should show LISTEN on 127.0.0.1:8000
curl -I http://127.0.0.1:8000 # expect HTTP/1.1 200 OK
curl -I -H "Host: nanotrace.org" http://127.0.0.1/ # through NGINX, expect 200 OK
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:42:01 UTC; 23ms ago
Main PID: 6864 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 3.4M (peak: 3.4M)
CPU: 17ms
CGroup: /system.slice/nanotrace.service
└─6864 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --chdir /home/mi…
Aug 26 22:42:01 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
curl: (7) Failed to connect to 127.0.0.1 port 8000 after 0 ms: Couldn't connect to server
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 26 Aug 2025 22:42:01 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
(venv) michal@quantworld:~/NanoTrace$ sudo tail -n 50 /var/log/nanotrace_error.log
tail: cannot open '/var/log/nanotrace_error.log' for reading: No such file or directory
(venv) michal@quantworld:~/NanoTrace$ # 1) ensure packages
touch ~/NanoTrace/backend/__init__.py
touch ~/NanoTrace/backend/config/__init__.py
# 2) __init__.py (Flask factory) — use package-relative imports
# from config.config -> backend.config.config
# from app.views.... -> .views....
sed -i 's/from config.config import Config/from backend.config.config import Config/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.auth/from .views.auth/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.main/from .views.main/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.certificates/from .views.certificates/' \
~/NanoTrace/backend/app/__init__.py
sed -i 's/from app.views.admin/from .views.admin/' \
~/NanoTrace/backend/app/__init__.py
# 3) views — import db/login_manager from the package (.. means "backend.app")
sed -i 's/from app import db/from .. import db/' \
~/NanoTrace/backend/app/views/certificates.py
sed -i 's/from app import db/from .. import db/' \
~/NanoTrace/backend/app/views/admin.py
sed -i 's/from app import db, login_manager/from .. import db, login_manager/' \
~/NanoTrace/backend/app/views/auth.py 2>/dev/null || true
# 4) models — already relative in your setup, but ensure user model uses relative
sed -i 's/from .. import db, login_manager/from .. import db, login_manager/' \
~/NanoTrace/backend/app/models/user.py
# 5) app entrypoints
# backend/app.py should import from backend.app (for the dev run)
sed -i 's/^from app import create_app$/from backend.app import create_app/' \
~/NanoTrace/backend/app.py
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
source venv/bin/activate
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/michal/NanoTrace/backend/wsgi.py", line 2, in <module>
app = create_app()
^^^^^^^^^^^^
File "/home/michal/NanoTrace/backend/app/__init__.py", line 25, in create_app
from .views.auth import bp as auth_bp
File "/home/michal/NanoTrace/backend/app/views/auth.py", line 3, in <module>
from app.models.user import User
ModuleNotFoundError: No module named 'app'
(venv) michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/backend/app/views/auth.py
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/michal/NanoTrace/backend/wsgi.py", line 2, in <module>
app = create_app()
^^^^^^^^^^^^
File "/home/michal/NanoTrace/backend/app/__init__.py", line 25, in create_app
from .views.auth import bp as auth_bp
File "/home/michal/NanoTrace/backend/app/views/auth.py", line 4, in <module>
from app import db
ModuleNotFoundError: No module named 'app'
(venv) michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/backend/app/views/auth.py
(venv) michal@quantworld:~/NanoTrace$ grep -R "from app" ~/NanoTrace/backend/app
/home/michal/NanoTrace/backend/app/views/admin.py:from app.models.certificate import Certificate
/home/michal/NanoTrace/backend/app/views/certificates.py:from app.models.certificate import Certificate
(venv) michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/backend/app/views/admin.py
(venv) michal@quantworld:~/NanoTrace$ nano ~/NanoTrace/backend/app/views/certificates.py
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
WSGI app loaded OK: <Flask 'backend.app'>
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Excellent — that’s the breakthrough 🎉
Your test confirmed:
luaWSGI app loaded OK: <Flask 'backend.app'>
That means:
-
All the
from app ...imports have been corrected to relative imports. -
Your Flask factory
create_app()is working. -
Gunicorn can now successfully load
backend.wsgi:app.
✅ Next Steps
-
Restart Gunicorn service with the corrected code:
bashsudo systemctl daemon-reexec sudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
-
Confirm Gunicorn is bound to 127.0.0.1:8000:
bashsudo ss -tlnp | grep 8000
You should see something like:
nginxLISTEN 0 128 127.0.0.1:8000 ...
-
Test directly (bypassing NGINX):
bashcurl -I http://127.0.0.1:8000
Expect:
HTTP/1.1 200 OK
-
Test via NGINX:
bashcurl -I -H "Host: nanotrace.org" http://127.0.0.1/
Expect:
HTTP/1.1 200 OK
Would you like me to also update your nanotrace.service file so it explicitly logs errors into /var/log/nanotrace_error.log and /var/log/nanotrace_access.log?
That way, if something breaks again, you’ll have clean logs instead of chasing systemd output.
You said:
(venv) michal@quantworld:~/NanoTrace$ cd ~/NanoTrace
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("WSGI app loaded OK:", app)
PY
WSGI app loaded OK: <Flask 'backend.app'>
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl daemon-reexec
sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
● nanotrace.service - NanoTrace Flask app with Gunicorn
Loaded: loaded (/etc/systemd/system/nanotrace.service; disabled; preset: enabled)
Active: active (running) since Tue 2025-08-26 22:53:09 UTC; 22ms ago
Main PID: 7056 (gunicorn)
Tasks: 1 (limit: 19091)
Memory: 3.8M (peak: 3.8M)
CPU: 16ms
CGroup: /system.slice/nanotrace.service
└─7056 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --chdir /home/mi…
Aug 26 22:53:09 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
(venv) michal@quantworld:~/NanoTrace$ sudo ss -tlnp | grep 8000
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfect troubleshooting so far — the Flask app is loading, Gunicorn starts, but it isn’t actually binding to 127.0.0.1:8000. That’s why ss shows nothing on port 8000.
By default Gunicorn binds to 127.0.0.1:8000 only if told to. In your nanotrace.service we probably didn’t specify --bind. Let’s fix that.
1. Edit your systemd service
Open:
bashsudo nano /etc/systemd/system/nanotrace.service
And make sure the ExecStart line looks like this (adjust paths if needed):
ini[Unit]
Description=NanoTrace Flask app with Gunicorn
After=network.target
[Service]
User=michal
Group=michal
WorkingDirectory=/home/michal/NanoTrace
Environment="PYTHONPATH=/home/michal/NanoTrace"
ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn \
--workers 3 \
--bind 127.0.0.1:8000 \
backend.wsgi:app
Restart=always
[Install]
WantedBy=multi-user.target
Key things:
-
--bind 127.0.0.1:8000→ forces Gunicorn to listen there -
backend.wsgi:app→ correct entrypoint -
PYTHONPATHis set sobackendis importable
2. Reload systemd & restart service
bashsudo systemctl daemon-reexec sudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
3. Confirm Gunicorn listening
bashsudo ss -tlnp | grep 8000
Should show:
nginxLISTEN 0 128 127.0.0.1:8000 ...
4. Test directly
bashcurl -I http://127.0.0.1:8000
Expect 200 OK.
Want me to also extend this so Gunicorn logs go into /var/log/nanotrace/ instead of only journalctl? That way, you’ll have clean error/access logs like a production server.
You said:
[Unit]
Description=NanoTrace Flask app with Gunicorn
After=network.target
[Service]
User=michal
Group=www-data
WorkingDirectory=/home/michal/NanoTrace
Environment="PATH=/home/michal/NanoTrace/venv/bin"
Environment="PYTHONPATH=/home/michal/NanoTrace"
ExecStart=/home/michal/NanoTrace/venv/bin/gunicorn \
--chdir /home/michal/NanoTrace \
--workers 3 \
--bind 127.0.0.1:8000 \
--log-level info \
--access-logfile /var/log/nanotrace_access.log \
--error-logfile /var/log/nanotrace_error.log \
backend.wsgi:app
Restart=always
[Install]
WantedBy=multi-user.target
ChatGPT said:
That’s almost perfect — just two small adjustments to make sure it works smoothly:
1. Log directory
Right now Gunicorn will fail to write logs if /var/log/nanotrace_* doesn’t exist or isn’t writable by michal.
So create a dedicated directory and adjust permissions:
bashsudo mkdir -p /var/log/nanotrace
sudo touch /var/log/nanotrace/access.log /var/log/nanotrace/error.log
sudo chown -R michal:www-data /var/log/nanotrace
Then update your unit file to point inside that directory:
ini--access-logfile /var/log/nanotrace/access.log \ --error-logfile /var/log/nanotrace/error.log \
2. Double-check PATH
You already set Environment="PATH=..." so Gunicorn uses the venv, good.
Keep Environment="PYTHONPATH=/home/michal/NanoTrace" so the backend package resolves.
3. Reload + restart
After editing the service:
bashsudo systemctl daemon-reexec sudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
4. Verify
Check Gunicorn is listening:
bashsudo ss -tlnp | grep 8000
Check logs:
bashtail -n 50 /var/log/nanotrace/error.log
tail -n 20 /var/log/nanotrace/access.log
Test through both Gunicorn and NGINX:
bashcurl -I http://127.0.0.1:8000
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
Do you want me to also configure log rotation (logrotate) so those Gunicorn logs don’t grow forever?
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo ss -tlnp | grep 8000
(venv) michal@quantworld:~/NanoTrace$ nothing
ChatGPT said:
Got it — if nothing is listening on 127.0.0.1:8000, then Gunicorn didn’t bind correctly even though systemctl status shows the service as “active”.
That usually means it crashed right after startup (e.g. import error, or log path issue).
Let’s debug in 3 quick steps:
1. Check service logs
bashsudo journalctl -u nanotrace -n 50 --no-pager
This will show why Gunicorn isn’t holding port 8000.
2. Check error log
If you already pointed logs to /var/log/nanotrace/error.log, check:
bashtail -n 50 /var/log/nanotrace/error.log
3. Manual Gunicorn run (inside venv)
Try starting it directly to see errors immediately:
bashcd ~/NanoTrace
source venv/bin/activate
gunicorn --chdir /home/michal/NanoTrace --bind 127.0.0.1:8000 backend.wsgi:app
If it fails, we’ll see exactly what exception is raised.
👉 Run step 3 and paste me the output — that will tell us why Gunicorn isn’t staying up.
You said:
CGroup: /system.slice/nanotrace.service
└─7186 /home/michal/NanoTrace/venv/bin/python3 /home/michal/NanoTrace/venv/bin/gunicorn --chdir /home/mi…
Aug 26 22:56:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
(venv) michal@quantworld:~/NanoTrace$ sudo ss -tlnp | grep 8000
(venv) michal@quantworld:~/NanoTrace$ sudo journalctl -u nanotrace -n 50 --no-pager
Aug 26 22:53:09 quantworld gunicorn[7062]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 2.
Aug 26 22:53:09 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:53:09 quantworld gunicorn[7064]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 3.
Aug 26 22:53:09 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:53:09 quantworld gunicorn[7066]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 4.
Aug 26 22:53:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:53:10 quantworld gunicorn[7068]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 5.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld gunicorn[7186]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 1.
Aug 26 22:56:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld gunicorn[7192]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 2.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7194]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 3.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7196]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 4.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7198]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 5.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfect, we found the culprit:
bashError: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Gunicorn is crashing because it cannot write to /var/log/nanotrace_error.log.
Systemd then keeps restarting it, but it always fails before binding port 8000.
✅ Fix
Let’s give Gunicorn a place to log:
bashsudo mkdir -p /var/log/nanotrace
sudo touch /var/log/nanotrace/nanotrace_error.log
sudo touch /var/log/nanotrace/nanotrace_access.log
sudo chown -R michal:www-data /var/log/nanotrace
sudo chmod 664 /var/log/nanotrace/*.log
Then update your nanotrace.service so it points inside /var/log/nanotrace/:
ini--access-logfile /var/log/nanotrace/nanotrace_access.log \ --error-logfile /var/log/nanotrace/nanotrace_error.log \
Restart & Verify
bashsudo systemctl daemon-reexec sudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
Now check if Gunicorn is listening:
bashsudo ss -tlnp | grep 8000
If it’s good, test:
bashcurl -I http://127.0.0.1:8000
curl -I -H "Host: nanotrace.org" http://127.0.0.1/
👉 Do you want me to also set journald logging instead of file logs? That way Gunicorn writes only to journalctl and you don’t need to manage log files at all (simpler).
You said:
how?
ChatGPT said:
Good question — journald logging is the cleanest setup for systemd services because it avoids permission headaches with /var/log/* files.
Right now your service uses:
ini--access-logfile /var/log/nanotrace/nanotrace_access.log \ --error-logfile /var/log/nanotrace/nanotrace_error.log \
✅ Switch to journald logging
-
Edit your service file:
bashsudo nano /etc/systemd/system/nanotrace.service
-
Change the ExecStart to remove the
--access-logfileand--error-logfileoptions, and instead add--access-logfile - --error-logfile -so logs go to stdout/stderr (journald picks them up):
iniExecStart=/home/michal/NanoTrace/venv/bin/gunicorn \
--chdir /home/michal/NanoTrace \
--workers 3 \
--bind 127.0.0.1:8000 \
--log-level info \
--access-logfile - \
--error-logfile - \
backend.wsgi:app
-
Reload systemd:
bashsudo systemctl daemon-reexec sudo systemctl restart nanotrace
-
Check logs via journald:
bashsudo journalctl -u nanotrace -n 50 --no-pager
🔍 Why better?
-
No need to create
/var/log/nanotrace/*. -
Logs rotate automatically with journald.
-
You can filter with
journalctl -u nanotraceinstead of tailing files.
Do you want me to edit your nanotrace.service now to the journald-friendly version so you can paste it directly?
You said:
r(13, 'Permission denied')]
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 5.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld gunicorn[7186]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 1.
Aug 26 22:56:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld gunicorn[7192]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 2.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7194]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 3.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7196]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 4.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7198]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 5.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
(venv) michal@quantworld:~/NanoTrace$ sudo mkdir -p /var/log/nanotrace
sudo touch /var/log/nanotrace/nanotrace_error.log
sudo touch /var/log/nanotrace/nanotrace_access.log
sudo chown -R michal:www-data /var/log/nanotrace
sudo chmod 664 /var/log/nanotrace/*.log
(venv) michal@quantworld:~/NanoTrace$ sudo nano /etc/systemd/system/nanotrace.service
(venv) michal@quantworld:~/NanoTrace$ sudo systemctl daemon-reexec
sudo systemctl restart nanotrace
(venv) michal@quantworld:~/NanoTrace$ sudo journalctl -u nanotrace -n 50 --no-pager
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 3.
Aug 26 22:53:09 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:53:09 quantworld gunicorn[7066]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:53:09 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 4.
Aug 26 22:53:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:53:10 quantworld gunicorn[7068]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 5.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:53:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:53:10 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld gunicorn[7186]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 1.
Aug 26 22:56:10 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:10 quantworld gunicorn[7192]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:10 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 2.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7194]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 3.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7196]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 4.
Aug 26 22:56:11 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 22:56:11 quantworld gunicorn[7198]: Error: Error: '/var/log/nanotrace_error.log' isn't writable [PermissionError(13, 'Permission denied')]
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Main process exited, code=exited, status=1/FAILURE
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Scheduled restart job, restart counter is at 5.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Start request repeated too quickly.
Aug 26 22:56:11 quantworld systemd[1]: nanotrace.service: Failed with result 'exit-code'.
Aug 26 22:56:11 quantworld systemd[1]: Failed to start nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 23:00:14 quantworld systemd[1]: Started nanotrace.service - NanoTrace Flask app with Gunicorn.
Aug 26 23:00:14 quantworld gunicorn[7329]: [2025-08-26 23:00:14 +0000] [7329] [INFO] Starting gunicorn 21.2.0
Aug 26 23:00:14 quantworld gunicorn[7329]: [2025-08-26 23:00:14 +0000] [7329] [INFO] Listening at: http://127.0.0.1:8000 (7329)
Aug 26 23:00:14 quantworld gunicorn[7329]: [2025-08-26 23:00:14 +0000] [7329] [INFO] Using worker: sync
Aug 26 23:00:14 quantworld gunicorn[7330]: [2025-08-26 23:00:14 +0000] [7330] [INFO] Booting worker with pid: 7330
Aug 26 23:00:14 quantworld gunicorn[7331]: [2025-08-26 23:00:14 +0000] [7331] [INFO] Booting worker with pid: 7331
Aug 26 23:00:14 quantworld gunicorn[7332]: [2025-08-26 23:00:14 +0000] [7332] [INFO] Booting worker with pid: 7332
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfect, Michal — you nailed it 🎯
At first, Gunicorn was crashing because it couldn’t write to /var/log/nanotrace_error.log. After you created the directory and fixed permissions, Gunicorn finally started cleanly:
csharp[INFO] Listening at: http://127.0.0.1:8000 (7329)
[INFO] Using worker: sync
Booting worker with pid: 7330
That means:
-
✅ Gunicorn is bound on
127.0.0.1:8000 -
✅ Workers are running
-
✅ NGINX should now be able to proxy requests to it
Next sanity checks
-
Confirm it’s listening:
bashsudo ss -tlnp | grep 8000
You should see 127.0.0.1:8000 with Gunicorn.
-
Hit it directly (bypassing NGINX):
bashcurl -I http://127.0.0.1:8000
Expect:
HTTP/1.1 200 OK
-
Go through NGINX:
bashcurl -I -H "Host: nanotrace.org" http://127.0.0.1/
Expect 200 OK.
If both work, then visiting http://nanotrace.org in a browser should finally load your Flask app via Gunicorn+NGINX.
⚖️ About logs:
Right now logs go into /var/log/nanotrace/*.log. This works fine, but in production it’s simpler to let journald handle logs (--access-logfile - --error-logfile -). Then you just do:
bashsudo journalctl -u nanotrace -f
Do you want to keep file logs under /var/log/nanotrace/, or should we switch fully to journald for a cleaner setup?
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo ss -tlnp | grep 8000
LISTEN 0 2048 127.0.0.1:8000 0.0.0.0:* users:(("gunicorn",pid=7332,fd=5),("gunicorn",pid=7331,fd=5),("gunicorn",pid=7330,fd=5),("gunicorn",pid=7329,fd=5))
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Excellent 👌 — now Gunicorn is listening on 127.0.0.1:8000 with three workers.
That means the backend is healthy, and NGINX should be able to talk to it.
Next quick tests
-
Direct check (bypass NGINX):
bashcurl -I http://127.0.0.1:8000
Expected: HTTP/1.1 200 OK (from Flask/Gunicorn).
-
Through NGINX (full path):
bashcurl -I -H "Host: nanotrace.org" http://127.0.0.1/
Expected: also HTTP/1.1 200 OK.
If this still gives 502, it means the NGINX proxy_pass isn’t pointing at 127.0.0.1:8000 correctly.
Would you like me to double-check your /etc/nginx/sites-available/nanotrace.conf and show you the exact location / block that should proxy to Gunicorn?
You said:
all 200 ok
ChatGPT said:
Perfect — that’s the green light ✅
Right now you’ve got:
-
Gunicorn running under systemd (
nanotrace.service), listening on127.0.0.1:8000. -
NGINX proxying from
nanotrace.org→127.0.0.1:8000. -
Both direct (
:8000) and via NGINX (nanotrace.org) return200 OK.
That means your backend is fully alive in production. 🎉
Next recommended steps
-
Enable autostart (so Gunicorn starts on reboot):
bashsudo systemctl enable nanotrace -
Clean up logs permissions (optional, you already fixed, but better to confirm):
bashsudo ls -l /var/log/nanotrace/ -
Get SSL working (Let’s Encrypt):
Now that DNS + HTTP are confirmed working, rerun:bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.orgThis time it should pass validation.
Want me to walk you through the SSL setup now so you can access https://nanotrace.org securely?
You said:
(venv) michal@quantworld:~/NanoTrace$ sudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for nanotrace.org and www.nanotrace.org
Certbot failed to authenticate some domains (authenticator: nginx). The Certificate Authority reported these problems:
Domain: nanotrace.org
Type: dns
Detail: no valid A records found for nanotrace.org; no valid AAAA records found for nanotrace.org
Hint: The Certificate Authority failed to verify the temporary nginx configuration changes made by Certbot. Ensure the listed domains point to this nginx server and that it is accessible from the internet.
Some challenges have failed.
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Got it — this is a DNS propagation timing issue.
Certbot is saying:
pgsqlno valid A records found for nanotrace.org
But earlier you showed:
bashdig +short nanotrace.org 217.154.34.203
So your local resolver sees the record, but Let’s Encrypt’s servers (worldwide validation) don’t see it yet. This usually means:
-
DNS records were updated very recently (minutes–hours).
-
TTL (time-to-live) still needs to expire worldwide.
-
Some resolvers may take up to 24h, but usually <2h.
✅ What to do now
-
Double-check your DNS provider’s records:
-
Arecord →nanotrace.org→217.154.34.203 -
Arecord →www.nanotrace.org→217.154.34.203
-
-
Test from a global perspective:
bashdig nanotrace.org @8.8.8.8 dig nanotrace.org @1.1.1.1(this queries Google + Cloudflare resolvers directly)
-
Wait a little — then re-run:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
⚡ Tip: If you want HTTPS working immediately while waiting for DNS, we can issue a temporary self-signed certificate so that NGINX serves TLS right away. Then replace with Let’s Encrypt when DNS is ready.
Do you want me to guide you through adding a temporary self-signed SSL cert so https://nanotrace.org is accessible today while DNS finishes propagating?
You said:
for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.
(venv) michal@quantworld:~/NanoTrace$ dig nanotrace.org @8.8.8.8
dig nanotrace.org @1.1.1.1
; <<>> DiG 9.18.30-0ubuntu0.24.04.2-Ubuntu <<>> nanotrace.org @8.8.8.8
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61314
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;nanotrace.org. IN A
;; AUTHORITY SECTION:
nanotrace.org. 1800 IN SOA ns1.livedns.co.uk. admin.nanotrace.org. 1745738499 10800 3600 604800 3600
;; Query time: 13 msec
;; SERVER: 8.8.8.8#53(8.8.8.8) (UDP)
;; WHEN: Tue Aug 26 23:05:25 UTC 2025
;; MSG SIZE rcvd: 101
; <<>> DiG 9.18.30-0ubuntu0.24.04.2-Ubuntu <<>> nanotrace.org @1.1.1.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48408
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;nanotrace.org. IN A
;; AUTHORITY SECTION:
nanotrace.org. 3600 IN SOA ns1.livedns.co.uk. admin.nanotrace.org. 1745738499 10800 3600 604800 3600
;; Query time: 18 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
;; WHEN: Tue Aug 26 23:05:25 UTC 2025
;; MSG SIZE rcvd: 101
(venv) michal@quantworld:~/NanoTrace$
ChatGPT said:
Perfect debugging step — thanks for running that!
Here’s the situation:
-
digwith Google DNS (8.8.8.8) and Cloudflare (1.1.1.1) shows:yamlANSWER: 0 AUTHORITY SECTION: nanotrace.org. SOA ns1.livedns.co.uk.→ That means your registrar’s nameservers are live, but no
Arecord is visible globally yet. -
Locally, you saw
dig +short nanotrace.org = 217.154.34.203earlier — that was likely your ISP’s DNS cache showing your just-added record.
✅ What this means
-
Your DNS provider (looks like 123-Reg / Livedns UK) has accepted the record, but it has not propagated to the authoritative nameservers yet.
-
Until that happens, Let’s Encrypt (and anyone outside your ISP) cannot resolve
nanotrace.org.
🔧 What you should do now
-
Go into your DNS management panel.
Make sure you explicitly have:-
Arecord →nanotrace.org→217.154.34.203 -
Arecord →www.nanotrace.org→217.154.34.203
TTL = 3600 (1 hour) or lower while testing.
-
-
Verify with the authoritative nameservers directly:
bashdig @ns1.livedns.co.uk nanotrace.org A dig @ns2.livedns.co.uk nanotrace.org A dig @ns3.livedns.co.uk nanotrace.org AIf those return no
Arecord, then the record has not been published yet. -
Wait for propagation. Sometimes it’s 10–30 minutes, sometimes a few hours.
⚡ Next step for Certbot:
Once dig @8.8.8.8 nanotrace.org returns your IP, just re-run:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org
👉 Do you want me to show you the exact dig commands with your authoritative DNS servers (ns1.livedns.co.uk, etc.) so you can confirm if the record is actually published already? That will tell us if it’s just propagation delay or if the record wasn’t added correctly.
You said:
all subdomains and www.nanotrace.org propagated now nanotrace.org is propagating
ChatGPT said:
Excellent — that matches what we saw: subdomains + www.nanotrace.org are already visible globally, but the apex domain nanotrace.org is still catching up in propagation.
That’s totally normal — root domain (@ / naked domain) records often take a bit longer depending on the DNS provider’s SOA refresh intervals.
✅ Here’s what you should do now:
-
Confirm propagation for apex:
Run this against the authoritative nameservers directly:bashdig @ns1.livedns.co.uk nanotrace.org A +short dig @ns2.livedns.co.uk nanotrace.org A +short dig @ns3.livedns.co.uk nanotrace.org A +short-
If you see
217.154.34.203, it’s published. -
If still blank, it means the registrar hasn’t pushed the record live yet.
-
-
Once it resolves, re-run Certbot:
bashsudo certbot --nginx -d nanotrace.org -d www.nanotrace.org -
Optional safety check: Since all your subdomains already work, you could even grab a certificate just for
www.nanotrace.orgfirst:bashsudo certbot --nginx -d www.nanotrace.orgThat way, your site gets HTTPS immediately while waiting for the apex to finish propagating. Later you can expand the cert to include
nanotrace.org.
⚡ Question for you: do you want me to prepare the Certbot renewal strategy (so it auto-renews every 60 days with systemd + nginx reload), or do you prefer to first wait until the apex finishes propagating and only then automate?
You said:
let create sofisticated admin console in mid time: DASHBOARD_MAIN_TEMPLATE
# 10. Create additional admin templates and finalize setup
# User list template
cat > backend/app/admin/templates/users/list.html << 'USER_LIST_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}User Management{% endblock %}
{% block content %}
<!-- Header with search and filters -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">User Management</h2>
<div class="flex flex-col sm:flex-row gap-4">
<!-- Search -->
<div class="relative">
<input type="text" id="user-search" placeholder="Search users..."
value="{{ search }}"
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<!-- Role filter -->
<select id="role-filter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Roles</option>
<option value="user" {{ 'selected' if role_filter == 'user' }}>User</option>
<option value="admin" {{ 'selected' if role_filter == 'admin' }}>Admin</option>
<option value="super_admin" {{ 'selected' if role_filter == 'super_admin' }}>Super Admin</option>
</select>
<!-- Create user button -->
<a href="{{ url_for('admin.create_user') }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-plus mr-2"></i>Create User
</a>
</div>
</div>
</div>
<!-- Users table -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Certificates</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for user in users.items %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
{{ user.username[0].upper() }}
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.username }}</div>
<div class="text-sm text-gray-500">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-purple-100 text-purple-800' if user.role == 'super_admin'
else 'bg-blue-100 text-blue-800' if user.role == 'admin'
else 'bg-gray-100 text-gray-800' }}">
{{ user.role.replace('_', ' ').title() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if user.is_active else 'error' }}"></span>
<span class="text-sm text-gray-900">
{{ 'Active' if user.is_active else 'Inactive' }}
{% if user.is_verified %}
<i class="fas fa-check-circle text-green-500 ml-1" title="Verified"></i>
{% endif %}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ user.certificates.count() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="text-blue-600 hover:text-blue-900">View</a>
<a href="{{ url_for('admin.edit_user', user_id=user.id) }}"
class="text-indigo-600 hover:text-indigo-900">Edit</a>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | lower }})"
class="text-{{ 'red' if user.is_active else 'green' }}-600 hover:text-{{ 'red' if user.is_active else 'green' }}-900">
{{ 'Deactivate' if user.is_active else 'Activate' }}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if users.pages > 1 %}
<div class="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
<div class="flex items-center justify-between">
<div class="flex-1 flex justify-between sm:hidden">
{% if users.has_prev %}
<a href="{{ url_for('admin.users', page=users.prev_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Previous</a>
{% endif %}
{% if users.has_next %}
<a href="{{ url_for('admin.users', page=users.next_num, search=search, role=role_filter) }}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Next</a>
{% endif %}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing <span class="font-medium">{{ users.per_page * (users.page - 1) + 1 }}</span>
to <span class="font-medium">{{ users.per_page * users.page if users.page < users.pages else users.total }}</span>
of <span class="font-medium">{{ users.total }}</span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{% if users.has_prev %}
<a href="{{ url_for('admin.users', page=users.prev_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<i class="fas fa-chevron-left"></i>
</a>
{% endif %}
{% for page_num in users.iter_pages() %}
{% if page_num %}
{% if page_num != users.page %}
<a href="{{ url_for('admin.users', page=page_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
{{ page_num }}
</a>
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">
{{ page_num }}
</span>
{% endif %}
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>
{% endif %}
{% endfor %}
{% if users.has_next %}
<a href="{{ url_for('admin.users', page=users.next_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<i class="fas fa-chevron-right"></i>
</a>
{% endif %}
</nav>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
// Search functionality
document.getElementById('user-search').addEventListener('input', function(e) {
const search = e.target.value;
const roleFilter = document.getElementById('role-filter').value;
updateUserFilter(search, roleFilter);
});
document.getElementById('role-filter').addEventListener('change', function(e) {
const roleFilter = e.target.value;
const search = document.getElementById('user-search').value;
updateUserFilter(search, roleFilter);
});
function updateUserFilter(search, role) {
const url = new URL(window.location);
url.searchParams.set('search', search);
url.searchParams.set('role', role);
url.searchParams.set('page', '1'); // Reset to first page
window.location.href = url.toString();
}
function toggleUserStatus(userId, isActive) {
const action = isActive ? 'deactivate' : 'activate';
if (confirm(
Are you sure you want to ${action} this user?)) {
showLoading();
fetch(/admin/users/${userId}/toggle-status, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
USER_LIST_TEMPLATE
# 11. Create Admin CSS and final integration scripts
cat > backend/app/admin/static/css/admin.css << 'ADMIN_CSS'
/* Custom admin dashboard styles */
.admin-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.metric-card {
transition: all 0.3s ease;
}
.metric-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.status-card {
border-left: 4px solid;
}
.status-card.healthy {
border-color: #10b981;
}
.status-card.warning {
border-color: #f59e0b;
}
.status-card.error {
border-color: #ef4444;
}
/* Loading animations */
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
/* Table enhancements */
.admin-table tbody tr:hover {
background-color: #f8fafc;
}
/* Chart containers */
.chart-container {
position: relative;
height: 300px;
}
/* Custom scrollbar */
.admin-scroll::-webkit-scrollbar {
width: 6px;
}
.admin-scroll::-webkit-scrollbar-track {
background: #f1f5f9;
}
.admin-scroll::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.admin-scroll::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Mobile responsive fixes */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
}
ADMIN_CSS
# 12. Create admin integration script
cat > integrate_admin.sh << 'INTEGRATE_ADMIN'
#!/bin/bash
set -e
echo "🔗 Integrating Admin Dashboard with Flask App..."
cd $PROJECT_ROOT
source venv/bin/activate
# Update main Flask app to register admin blueprint
cat >> backend/app/__init__.py << 'FLASK_ADMIN_INTEGRATION'
# Register admin blueprint
from app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
# Import admin models for database creation
from app.models import admin_models
FLASK_ADMIN_INTEGRATION
# Update User model to include admin functionality
cat >> backend/app/models/user.py << 'USER_ADMIN_FIELDS'
# Admin fields
role = db.Column(db.String(20), default='user')
is_verified = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True)
last_login = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@property
def is_admin(self):
return self.role in ['admin', 'super_admin']
def has_role(self, role):
return self.role == role
USER_ADMIN_FIELDS
# Update Certificate model with admin fields
cat >> backend/app/models/certificate.py << 'CERT_ADMIN_FIELDS'
# Admin approval fields
status = db.Column(db.String(20), default='pending') # pending, approved, rejected, revoked
approved_at = db.Column(db.DateTime)
approved_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejected_at = db.Column(db.DateTime)
rejected_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejection_reason = db.Column(db.Text)
revoked_at = db.Column(db.DateTime)
revoked_by = db.Column(db.Integer, db.ForeignKey('users.id'))
revocation_reason = db.Column(db.Text)
# QR Code path
qr_code_path = db.Column(db.String(255))
# Relationships
approver = db.relationship('User', foreign_keys=[approved_by], backref='approved_certificates')
rejecter = db.relationship('User', foreign_keys=[rejected_by], backref='rejected_certificates')
revoker = db.relationship('User', foreign_keys=[revoked_by], backref='revoked_certificates')
def generate_certificate_id(self):
"""Generate unique certificate ID"""
import uuid
import hashlib
unique_string = f"{self.product_name}_{self.user_id}_{uuid.uuid4().hex[:8]}"
self.certificate_id = hashlib.md5(unique_string.encode()).hexdigest()[:12].upper()
return self.certificate_id
CERT_ADMIN_FIELDS
# Create database migration for admin fields
echo "📊 Creating database migration for admin dashboard..."
flask db migrate -m "Add admin dashboard fields"
flask db upgrade
# Create default admin user
cat > scripts/create_admin.py << 'CREATE_ADMIN_SCRIPT'
#!/usr/bin/env python3
"""Create default admin user for NanoTrace"""
import sys
import os
sys.path.append('/home/michal/NanoTrace/backend')
from app import create_app, db
from app.models.user import User
import getpass
def create_admin():
app = create_app()
with app.app_context():
print("Creating NanoTrace Admin User")
print("=" * 30)
username = input("Admin username: ")
email = input("Admin email: ")
password = getpass.getpass("Admin password: ")
# Check if user already exists
existing_user = User.query.filter_by(username=username).first()
if existing_user:
print(f"User {username} already exists!")
return
existing_email = User.query.filter_by(email=email).first()
if existing_email:
print(f"Email {email} already exists!")
return
# Create admin user
admin_user = User(
username=username,
email=email,
role='super_admin',
is_verified=True,
is_active=True
)
admin_user.set_password(password)
db.session.add(admin_user)
db.session.commit()
print(f"✅ Admin user {username} created successfully!")
print(f"You can now login at: http://your-domain/admin/dashboard")
if __name__ == '__main__':
create_admin()
CREATE_ADMIN_SCRIPT
chmod +x scripts/create_admin.py
# Create systemd service monitoring script
cat > scripts/monitor_services.py << 'SERVICE_MONITOR_SCRIPT'
#!/usr/bin/env python3
"""Monitor system services and record metrics"""
import sys
import os
import time
import subprocess
from datetime import datetime
# Add backend to path
sys.path.append('/home/michal/NanoTrace/backend')
from app import create_app, db
from app.admin.utils import SystemMonitor
def monitor_services():
"""Monitor services and record metrics"""
app = create_app()
with app.app_context():
try:
# Record system metrics
SystemMonitor.record_metrics()
# Check services
services = ['postgresql', 'redis-server', 'nginx', 'nanotrace']
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
print(f"{datetime.now()}: {service} - {status}")
except Exception as e:
print(f"{datetime.now()}: Error checking {service}: {e}")
print(f"{datetime.now()}: Monitoring cycle completed")
except Exception as e:
print(f"{datetime.now()}: Error in monitoring: {e}")
if __name__ == '__main__':
monitor_services()
SERVICE_MONITOR_SCRIPT
chmod +x scripts/monitor_services.py
# Create cron job for monitoring
echo "⏰ Setting up monitoring cron job..."
(crontab -l 2>/dev/null; echo "*/5 * * * * cd /home/michal/NanoTrace && /home/michal/NanoTrace/venv/bin/python scripts/monitor_services.py >> /var/log/nanotrace_monitor.log 2>&1") | crontab -
echo "✅ Admin Dashboard Integration Complete!"
echo ""
echo "Next steps:"
echo "1. Run: python scripts/create_admin.py (to create admin user)"
echo "2. Access admin dashboard at: http://your-domain/admin/dashboard"
echo "3. Monitor logs at: tail -f /var/log/nanotrace_monitor.log"
echo ""
echo "Admin Features Available:"
echo " 📊 System monitoring with real-time metrics"
echo " 👥 User management (create, edit, activate/deactivate)"
echo " 📄 Certificate approval workflow"
echo " 🔗 Blockchain network control"
echo " ⚙️ System configuration management"
echo " 📋 Audit logging and activity tracking"
echo " 💾 Backup and cleanup utilities"
echo " 🔧 Service control and log viewing"
INTEGRATE_ADMIN
chmod +x integrate_admin.sh
echo "🎛️ Admin Dashboard Builder Complete!"
echo ""
echo "Generated Files:"
echo " 📁 backend/app/admin/ - Admin blueprint with views and templates"
echo " 📊 backend/app/models/admin_models.py - Admin data models"
echo " 🎨 backend/app/admin/templates/ - Professional admin UI templates"
echo " 🔧 scripts/create_admin.py - Admin user creation script"
echo " 📈 scripts/monitor_services.py - System monitoring script"
echo " 🔗 integrate_admin.sh - Integration script"
echo ""
echo "To complete setup, run:"
echo " ./integrate_admin.sh"
echo ""
echo "Admin Dashboard Features:"
echo " 🎯 Real-time system monitoring with charts"
echo " 👤 Complete user management system"
echo " 📋 Certificate approval workflow"
echo " ⛓️ Blockchain network management"
echo " 🔧 System configuration and maintenance"
echo " 📊 Analytics and reporting"
echo " 🔒 Audit logging and security"
echo " 💾 Backup and cleanup tools"
echo ""
echo "The dashboard provides a professional admin interface with:"
echo " • Modern responsive design with Tailwind CSS"
echo " • Real-time metrics and charts"
echo " • Role-based access control"
echo " • Comprehensive system management"
echo " • Mobile-friendly interface"
echo " • Dark/light theme support" }), 500
ADMIN_SYSTEM_VIEWS
# 9. Create Admin Dashboard Templates
# Base template
cat > backend/app/admin/templates/admin_base.html << 'ADMIN_BASE_TEMPLATE'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}NanoTrace Admin Dashboard{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.sidebar-transition {
transition: all 0.3s ease-in-out;
}
.admin-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-healthy { background-color: #10b981; }
.status-warning { background-color: #f59e0b; }
.status-error { background-color: #ef4444; }
.metric-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
</style>
</head>
<body class="bg-gray-100">
<div class="flex min-h-screen">
<!-- Sidebar -->
<aside class="bg-gray-900 text-white w-64 min-h-screen sidebar-transition" id="sidebar">
<div class="p-4">
<div class="flex items-center mb-8">
<i class="fas fa-cog text-2xl mr-3 text-blue-400"></i>
<h1 class="text-xl font-bold">Admin Panel</h1>
</div>
<nav class="space-y-2">
<a href="{{ url_for('admin.dashboard') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-tachometer-alt mr-3"></i>
Dashboard
</a>
<a href="{{ url_for('admin.users') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-users mr-3"></i>
Users
</a>
<a href="{{ url_for('admin.certificates') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-certificate mr-3"></i>
Certificates
</a>
<a href="{{ url_for('admin.blockchain_status') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-link mr-3"></i>
Blockchain
</a>
<a href="{{ url_for('admin.system_overview') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-server mr-3"></i>
System
</a>
<hr class="my-4 border-gray-700">
<a href="{{ url_for('main.index') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-home mr-3"></i>
Back to Site
</a>
<a href="{{ url_for('auth.logout') }}"
class="flex items-center px-4 py-2 text-red-400 hover:bg-red-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-sign-out-alt mr-3"></i>
Logout
</a>
</nav>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-hidden">
<!-- Top Header -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex items-center">
<button id="sidebar-toggle" class="text-gray-500 hover:text-gray-700 mr-4">
<i class="fas fa-bars"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">{% block page_title %}Dashboard{% endblock %}</h2>
</div>
<div class="flex items-center space-x-4">
<!-- System Status Indicator -->
<div class="flex items-center">
<span class="status-indicator status-healthy" id="system-status"></span>
<span class="text-sm text-gray-600">System Status</span>
</div>
<!-- User Info -->
<div class="flex items-center">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium mr-2">
{{ current_user.username[0].upper() }}
</div>
<span class="text-sm text-gray-700">{{ current_user.username }}</span>
</div>
</div>
</div>
</header>
<!-- Page Content -->
<div class="p-6 overflow-y-auto" style="height: calc(100vh - 73px);">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mb-4">
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} bg-{{ 'green' if category == 'success' else 'red' }}-100 border border-{{ 'green' if category == 'success' else 'red' }}-400 text-{{ 'green' if category == 'success' else 'red' }}-700 px-4 py-3 rounded mb-2">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 flex items-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<span>Loading...</span>
</div>
</div>
<!-- Scripts -->
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', function() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-translate-x-full');
});
// System status check
function checkSystemStatus() {
fetch('/admin/api/system-stats')
.then(response => response.json())
.then(data => {
const indicator = document.getElementById('system-status');
// Simple health check based on CPU and memory
const cpu = data.system.cpu?.percent || 0;
const memory = data.system.memory?.percent || 0;
if (cpu > 90 || memory > 90) {
indicator.className = 'status-indicator status-error';
} else if (cpu > 70 || memory > 70) {
indicator.className = 'status-indicator status-warning';
} else {
indicator.className = 'status-indicator status-healthy';
}
})
.catch(() => {
document.getElementById('system-status').className = 'status-indicator status-error';
});
}
// Check status every 30 seconds
checkSystemStatus();
setInterval(checkSystemStatus, 30000);
// Loading overlay functions
function showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
// Auto-hide alerts after 5 seconds
document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
alert.style.opacity = '0';
setTimeout(() => alert.remove(), 300);
}, 5000);
});
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>
ADMIN_BASE_TEMPLATE
# Main dashboard template
cat > backend/app/admin/templates/dashboard/main.html << 'DASHBOARD_MAIN_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<!-- Quick Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- System Health -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-heartbeat text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">System Health</p>
<p class="text-2xl font-bold text-gray-900">
{% if system_stats.cpu.percent < 80 %}Healthy{% else %}Warning{% endif %}
</p>
</div>
</div>
</div>
<!-- Total Users -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<i class="fas fa-users text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Users</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_users or 0 }}</p>
</div>
</div>
</div>
<!-- Certificates -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
<i class="fas fa-certificate text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Certificates</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_certificates or 0 }}</p>
<p class="text-xs text-gray-500">{{ db_stats.pending_certificates or 0 }} pending</p>
</div>
</div>
</div>
<!-- Blockchain Status -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<i class="fas fa-link text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Blockchain</p>
{% set healthy_containers = fabric_status.values() | selectattr('healthy') | list | length %}
<p class="text-2xl font-bold text-gray-900">
{{ healthy_containers }}/{{ fabric_status | length }}
</p>
<p class="text-xs text-gray-500">containers running</p>
</div>
</div>
</div>
</div>
<!-- System Overview -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- System Resources -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">System Resources</h3>
<button id="refresh-stats" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="space-y-4">
<!-- CPU Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>CPU Usage</span>
<span>{{ "%.1f" | format(system_stats.cpu.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ system_stats.cpu.percent }}%"></div>
</div>
</div>
<!-- Memory Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Memory Usage</span>
<span>{{ "%.1f" | format(system_stats.memory.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full" style="width: {{ system_stats.memory.percent }}%"></div>
</div>
</div>
<!-- Disk Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Disk Usage</span>
<span>{{ "%.1f" | format(system_stats.disk.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-orange-600 h-2 rounded-full" style="width: {{ system_stats.disk.percent }}%"></div>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-600">Memory:</span>
<span class="font-medium">{{ "%.1f" | format(system_stats.memory.used / (1024**3)) }} GB used</span>
</div>
<div>
<span class="text-gray-600">Disk:</span>
<span class="font-medium">{{ "%.1f" | format(system_stats.disk.free / (1024**3)) }} GB free</span>
</div>
</div>
</div>
</div>
<!-- Fabric Network Status -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Blockchain Network</h3>
<div class="flex space-x-2">
<button id="start-network" class="px-3 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600">
Start
</button>
<button id="stop-network" class="px-3 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600">
Stop
</button>
</div>
</div>
<div class="space-y-3">
{% for container_name, status in fabric_status.items() %}
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if status.healthy else 'error' }}"></span>
<span class="text-sm font-medium text-gray-700">{{ container_name }}</span>
</div>
<span class="text-xs px-2 py-1 rounded-full {{ 'bg-green-100 text-green-800' if status.healthy else 'bg-red-100 text-red-800' }}">
{{ status.status }}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- CPU Usage Chart -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">CPU Usage (24h)</h3>
<canvas id="cpu-chart" height="300"></canvas>
</div>
<!-- Memory Usage Chart -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Memory Usage (24h)</h3>
<canvas id="memory-chart" height="300"></canvas>
</div>
</div>
<!-- Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Pending Certificates -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Pending Certificates</h3>
<a href="{{ url_for('admin.certificates', status='pending') }}"
class="text-sm text-blue-600 hover:text-blue-800">View All</a>
</div>
<div id="pending-certificates" class="space-y-3">
<div class="text-center text-gray-500 py-4">
<i class="fas fa-spinner fa-spin"></i>
Loading...
</div>
</div>
</div>
<!-- Recent Audit Logs -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Activity</h3>
</div>
<div class="space-y-3">
{% for log in recent_logs[:10] %}
<div class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fas fa-user text-blue-600 text-xs"></i>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-900">
<span class="font-medium">{{ log.user.username if log.user else 'System' }}</span>
{{ log.action }}
</p>
<p class="text-xs text-gray-500">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load charts
loadCPUChart();
loadMemoryChart();
loadPendingCertificates();
// Network control buttons
document.getElementById('start-network').addEventListener('click', function() {
controlFabricNetwork('start');
});
document.getElementById('stop-network').addEventListener('click', function() {
controlFabricNetwork('stop');
});
// Refresh stats button
document.getElementById('refresh-stats').addEventListener('click', function() {
location.reload();
});
});
function loadCPUChart() {
fetch('/admin/api/metrics?metric=cpu_percent&hours=24')
.then(response => response.json())
.then(data => {
const ctx = document.getElementById('cpu-chart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => new Date(d.timestamp).toLocaleTimeString()),
datasets: [{
label: 'CPU %',
data: data.map(d => d.value),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
});
}
function loadMemoryChart() {
fetch('/admin/api/metrics?metric=memory_percent&hours=24')
.then(response => response.json())
.then(data => {
const ctx = document.getElementById('memory-chart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => new Date(d.timestamp).toLocaleTimeString()),
datasets: [{
label: 'Memory %',
data: data.map(d => d.value),
borderColor: 'rgb(16, 185, 129)',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
});
}
function loadPendingCertificates() {
fetch('/admin/certificates/pending')
.then(response => response.json())
.then(data => {
const container = document.getElementById('pending-certificates');
if (data.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-4">No pending certificates</div>';
return;
}
container.innerHTML = data.slice(0, 5).map(cert =>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p class="text-sm font-medium text-gray-900">${cert.product_name}</p>
<p class="text-xs text-gray-500">by ${cert.supplier} • ${cert.days_pending} days ago</p>
</div>
<a href="/admin/certificates/${cert.id}"
class="text-blue-600 hover:text-blue-800 text-sm">Review</a>
</div>
).join('');
});
}
function controlFabricNetwork(action) {
showLoading();
fetch(/admin/blockchain/${action}-network, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert(Network ${action} completed successfully);
setTimeout(() => location.reload(), 2000);
} else {
alert(Failed to ${action} network: ${data.error});
}
})
.catch(error => {
hideLoading();
alert(Error: ${error.message});
});
}
</script>
{% endblock %}
DASHBOARD_MAIN_TEMPLATE#!/bin/bash
# =============================================================================
# NanoTrace Admin Dashboard Builder
# Creates a sophisticated Flask admin dashboard for system management
# =============================================================================
set -e
echo "🎛️ Building NanoTrace Admin Dashboard..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# Create admin blueprint structure
mkdir -p backend/app/admin/{views,templates,static}
mkdir -p backend/app/admin/static/{css,js}
mkdir -p backend/app/admin/templates/{base,dashboard,users,certificates,blockchain,system}
# 1. Create Admin Blueprint
cat > backend/app/admin/__init__.py << 'ADMIN_INIT'
from flask import Blueprint
bp = Blueprint('admin', __name__,
template_folder='templates',
static_folder='static',
static_url_path='/admin/static')
from app.admin.views import main, users, certificates, blockchain, system
ADMIN_INIT
# 2. Create Admin Models
cat > backend/app/models/admin_models.py << 'ADMIN_MODELS'
from datetime import datetime
from app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import enum
class UserRole(enum.Enum):
USER = "user"
ADMIN = "admin"
SUPER_ADMIN = "super_admin"
class SystemMetric(db.Model):
"""Track system metrics and health"""
__tablename__ = 'system_metrics'
id = db.Column(db.Integer, primary_key=True)
metric_name = db.Column(db.String(100), nullable=False)
metric_value = db.Column(db.Float, nullable=False)
metric_unit = db.Column(db.String(20))
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.metric_name,
'value': self.metric_value,
'unit': self.metric_unit,
'timestamp': self.timestamp.isoformat()
}
class AuditLog(db.Model):
"""Track admin actions for security"""
__tablename__ = 'audit_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
action = db.Column(db.String(200), nullable=False)
resource = db.Column(db.String(100))
resource_id = db.Column(db.String(100))
ip_address = db.Column(db.String(45))
user_agent = db.Column(db.Text)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref=db.backref('audit_logs', lazy=True))
def to_dict(self):
return {
'id': self.id,
'user': self.user.username if self.user else 'Unknown',
'action': self.action,
'resource': self.resource,
'resource_id': self.resource_id,
'ip': self.ip_address,
'timestamp': self.timestamp.isoformat()
}
class SystemConfig(db.Model):
"""Store system configuration settings"""
__tablename__ = 'system_config'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.Text)
description = db.Column(db.Text)
updated_by = db.Column(db.Integer, db.ForeignKey('users.id'))
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
updater = db.relationship('User')
@staticmethod
def get_config(key, default=None):
config = SystemConfig.query.filter_by(key=key).first()
return config.value if config else default
@staticmethod
def set_config(key, value, user_id, description=None):
config = SystemConfig.query.filter_by(key=key).first()
if config:
config.value = value
config.updated_by = user_id
config.updated_at = datetime.utcnow()
else:
config = SystemConfig(
key=key,
value=value,
description=description,
updated_by=user_id
)
db.session.add(config)
db.session.commit()
return config
ADMIN_MODELS
# 3. Create Admin Utils
cat > backend/app/admin/utils.py << 'ADMIN_UTILS'
from functools import wraps
from flask import abort, request, current_app
from flask_login import current_user
import psutil
import time
import requests
from datetime import datetime, timedelta
from app.models.admin_models import SystemMetric, AuditLog
from app import db
def admin_required(f):
"""Decorator to require admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role not in ['admin', 'super_admin']:
abort(403)
return f(*args, **kwargs)
return decorated_function
def super_admin_required(f):
"""Decorator to require super admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != 'super_admin':
abort(403)
return f(*args, **kwargs)
return decorated_function
def log_admin_action(action, resource=None, resource_id=None):
"""Log admin actions for audit trail"""
try:
log_entry = AuditLog(
user_id=current_user.id,
action=action,
resource=resource,
resource_id=str(resource_id) if resource_id else None,
ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent', '')[:500]
)
db.session.add(log_entry)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Failed to log admin action: {e}")
class SystemMonitor:
"""System monitoring utilities"""
@staticmethod
def get_system_stats():
"""Get current system statistics"""
try:
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
# Network stats (simplified)
network = psutil.net_io_counters()
return {
'cpu': {
'percent': cpu_percent,
'count': psutil.cpu_count()
},
'memory': {
'total': memory.total,
'available': memory.available,
'percent': memory.percent,
'used': memory.used
},
'disk': {
'total': disk.total,
'used': disk.used,
'free': disk.free,
'percent': (disk.used / disk.total) * 100
},
'network': {
'bytes_sent': network.bytes_sent,
'bytes_recv': network.bytes_recv
},
'timestamp': datetime.utcnow().isoformat()
}
except Exception as e:
current_app.logger.error(f"Error getting system stats: {e}")
return {}
@staticmethod
def record_metrics():
"""Record system metrics to database"""
stats = SystemMonitor.get_system_stats()
if not stats:
return
try:
metrics = [
SystemMetric(metric_name='cpu_percent', metric_value=stats['cpu']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_percent', metric_value=stats['memory']['percent'], metric_unit='%'),
SystemMetric(metric_name='disk_percent', metric_value=stats['disk']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_used', metric_value=stats['memory']['used'], metric_unit='bytes'),
SystemMetric(metric_name='disk_used', metric_value=stats['disk']['used'], metric_unit='bytes'),
]
for metric in metrics:
db.session.add(metric)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Error recording metrics: {e}")
db.session.rollback()
@staticmethod
def check_fabric_network():
"""Check Hyperledger Fabric network status"""
try:
# Check if Fabric containers are running
import docker
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com'
]
container_status = {}
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container_status[container_name] = {
'status': container.status,
'healthy': container.status == 'running'
}
except:
container_status[container_name] = {
'status': 'not_found',
'healthy': False
}
return container_status
except Exception as e:
current_app.logger.error(f"Error checking Fabric network: {e}")
return {}
@staticmethod
def get_database_stats():
"""Get database statistics"""
try:
from app.models.user import User
from app.models.certificate import Certificate
stats = {
'total_users': User.query.count(),
'admin_users': User.query.filter_by(role='admin').count(),
'total_certificates': Certificate.query.count(),
'pending_certificates': Certificate.query.filter_by(status='pending').count(),
'approved_certificates': Certificate.query.filter_by(status='approved').count(),
'rejected_certificates': Certificate.query.filter_by(status='rejected').count(),
}
return stats
except Exception as e:
current_app.logger.error(f"Error getting database stats: {e}")
return {}
def format_bytes(bytes):
"""Format bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes < 1024.0:
return f"{bytes:.1f} {unit}"
bytes /= 1024.0
return f"{bytes:.1f} PB"
def format_uptime(seconds):
"""Format uptime in human readable format"""
days = seconds // 86400
hours = (seconds % 86400) // 3600
minutes = (seconds % 3600) // 60
parts = []
if days:
parts.append(f"{int(days)}d")
if hours:
parts.append(f"{int(hours)}h")
if minutes:
parts.append(f"{int(minutes)}m")
return " ".join(parts) if parts else "0m"
ADMIN_UTILS
# 4. Create Main Admin Views
cat > backend/app/admin/views/main.py << 'ADMIN_MAIN_VIEWS'
from flask import render_template, jsonify, request, current_app
from app.admin import bp
from app.admin.utils import admin_required, SystemMonitor, log_admin_action
from app.models.admin_models import SystemMetric, AuditLog
from datetime import datetime, timedelta
from sqlalchemy import func
@bp.route('/dashboard')
@admin_required
def dashboard():
"""Main admin dashboard"""
log_admin_action("Accessed admin dashboard")
# Get system stats
system_stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
fabric_status = SystemMonitor.check_fabric_network()
# Get recent metrics for charts (last 24 hours)
since = datetime.utcnow() - timedelta(hours=24)
recent_metrics = SystemMetric.query.filter(
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.desc()).limit(100).all()
# Get recent audit logs
recent_logs = AuditLog.query.order_by(
AuditLog.timestamp.desc()
).limit(20).all()
return render_template('admin/dashboard/main.html',
system_stats=system_stats,
db_stats=db_stats,
fabric_status=fabric_status,
recent_metrics=recent_metrics,
recent_logs=recent_logs)
@bp.route('/api/system-stats')
@admin_required
def api_system_stats():
"""API endpoint for real-time system stats"""
stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
return jsonify({
'system': stats,
'database': db_stats,
'timestamp': datetime.utcnow().isoformat()
})
@bp.route('/api/metrics')
@admin_required
def api_metrics():
"""API endpoint for metrics data"""
hours = request.args.get('hours', 24, type=int)
metric_name = request.args.get('metric', 'cpu_percent')
since = datetime.utcnow() - timedelta(hours=hours)
metrics = SystemMetric.query.filter(
SystemMetric.metric_name == metric_name,
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.asc()).all()
data = [{
'timestamp': metric.timestamp.isoformat(),
'value': metric.metric_value
} for metric in metrics]
return jsonify(data)
@bp.route('/api/audit-logs')
@admin_required
def api_audit_logs():
"""API endpoint for audit logs"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
logs = AuditLog.query.order_by(
AuditLog.timestamp.desc()
).paginate(page=page, per_page=per_page, error_out=False)
return jsonify({
'logs': [log.to_dict() for log in logs.items],
'total': logs.total,
'pages': logs.pages,
'current_page': page
})
@bp.route('/health-check')
@admin_required
def health_check():
"""Comprehensive system health check"""
log_admin_action("Performed health check")
health_status = {
'timestamp': datetime.utcnow().isoformat(),
'overall': 'healthy',
'checks': {}
}
# Database check
try:
from app import db
db.session.execute('SELECT 1').fetchone()
health_status['checks']['database'] = {
'status': 'healthy',
'message': 'Database connection successful'
}
except Exception as e:
health_status['checks']['database'] = {
'status': 'unhealthy',
'message': f'Database error: {str(e)}'
}
health_status['overall'] = 'degraded'
# Redis check (if configured)
try:
import redis
redis_client = redis.from_url(current_app.config.get('REDIS_URL', 'redis://localhost:6379'))
redis_client.ping()
health_status['checks']['redis'] = {
'status': 'healthy',
'message': 'Redis connection successful'
}
except Exception as e:
health_status['checks']['redis'] = {
'status': 'unhealthy',
'message': f'Redis error: {str(e)}'
}
health_status['overall'] = 'degraded'
# Fabric network check
fabric_status = SystemMonitor.check_fabric_network()
all_healthy = all(container['healthy'] for container in fabric_status.values())
health_status['checks']['fabric'] = {
'status': 'healthy' if all_healthy else 'unhealthy',
'message': f'Fabric containers: {len([c for c in fabric_status.values() if c["healthy"]])}/{len(fabric_status)} healthy',
'details': fabric_status
}
if not all_healthy:
health_status['overall'] = 'degraded'
# System resources check
system_stats = SystemMonitor.get_system_stats()
cpu_ok = system_stats.get('cpu', {}).get('percent', 0) < 90
memory_ok = system_stats.get('memory', {}).get('percent', 0) < 90
disk_ok = system_stats.get('disk', {}).get('percent', 0) < 90
health_status['checks']['system_resources'] = {
'status': 'healthy' if (cpu_ok and memory_ok and disk_ok) else 'warning',
'message': f'CPU: {system_stats.get("cpu", {}).get("percent", 0):.1f}%, Memory: {system_stats.get("memory", {}).get("percent", 0):.1f}%, Disk: {system_stats.get("disk", {}).get("percent", 0):.1f}%'
}
if not (cpu_ok and memory_ok and disk_ok):
health_status['overall'] = 'warning'
return jsonify(health_status)
ADMIN_MAIN_VIEWS
# 5. Create User Management Views
cat > backend/app/admin/views/users.py << 'ADMIN_USER_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from app.admin import bp
from app.admin.utils import admin_required, super_admin_required, log_admin_action
from app.models.user import User
from app import db
from datetime import datetime
from werkzeug.security import generate_password_hash
@bp.route('/users')
@admin_required
def users():
"""User management page"""
log_admin_action("Accessed user management")
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
role_filter = request.args.get('role', '')
query = User.query
if search:
query = query.filter(
db.or_(
User.username.contains(search),
User.email.contains(search)
)
)
if role_filter:
query = query.filter(User.role == role_filter)
users = query.paginate(
page=page, per_page=25, error_out=False
)
return render_template('admin/users/list.html',
users=users,
search=search,
role_filter=role_filter)
@bp.route('/users/<int:user_id>')
@admin_required
def user_detail(user_id):
"""User detail page"""
user = User.query.get_or_404(user_id)
log_admin_action("Viewed user details", "user", user_id)
# Get user's certificates
certificates = user.certificates.order_by('created_at desc').limit(10).all()
# Get user's audit logs
audit_logs = user.audit_logs.order_by('timestamp desc').limit(20).all()
return render_template('admin/users/detail.html',
user=user,
certificates=certificates,
audit_logs=audit_logs)
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
@admin_required
def edit_user(user_id):
"""Edit user"""
user = User.query.get_or_404(user_id)
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
user.username = data.get('username', user.username)
user.email = data.get('email', user.email)
user.role = data.get('role', user.role)
user.is_verified = data.get('is_verified', user.is_verified) == 'true'
user.is_active = data.get('is_active', user.is_active) == 'true'
# Update password if provided
new_password = data.get('new_password')
if new_password:
user.password_hash = generate_password_hash(new_password)
try:
db.session.commit()
log_admin_action("Updated user", "user", user_id)
if request.is_json:
return jsonify({'success': True, 'message': 'User updated successfully'})
else:
flash('User updated successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user_id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error updating user: {str(e)}', 'error')
return render_template('admin/users/edit.html', user=user)
@bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
def toggle_user_status(user_id):
"""Toggle user active status"""
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
try:
db.session.commit()
action = "activated" if user.is_active else "deactivated"
log_admin_action(f"User {action}", "user", user_id)
return jsonify({
'success': True,
'message': f'User {action} successfully',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Create new user"""
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
try:
user = User(
username=data['username'],
email=data['email'],
role=data.get('role', 'user'),
is_verified=data.get('is_verified', False) == 'true',
is_active=data.get('is_active', True) == 'true'
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
log_admin_action("Created user", "user", user.id)
if request.is_json:
return jsonify({
'success': True,
'message': 'User created successfully',
'user_id': user.id
})
else:
flash('User created successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user.id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error creating user: {str(e)}', 'error')
return render_template('admin/users/create.html')
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
@super_admin_required
def delete_user(user_id):
"""Delete user (super admin only)"""
user = User.query.get_or_404(user_id)
try:
# Don't delete if user has certificates
if user.certificates.count() > 0:
return jsonify({
'success': False,
'error': 'Cannot delete user with existing certificates'
}), 400
username = user.username
db.session.delete(user)
db.session.commit()
log_admin_action("Deleted user", "user", user_id)
return jsonify({
'success': True,
'message': f'User {username} deleted successfully'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/stats')
@admin_required
def user_stats():
"""Get user statistics"""
from sqlalchemy import func
stats = {
'total_users': User.query.count(),
'active_users': User.query.filter_by(is_active=True).count(),
'verified_users': User.query.filter_by(is_verified=True).count(),
'admin_users': User.query.filter(User.role.in_(['admin', 'super_admin'])).count(),
'users_by_role': dict(
db.session.query(User.role, func.count(User.id))
.group_by(User.role)
.all()
),
'recent_signups': User.query.filter(
User.created_at >= datetime.utcnow() - timedelta(days=30)
).count()
}
return jsonify(stats)
ADMIN_USER_VIEWS
# 6. Create Certificate Management Views
cat > backend/app/admin/views/certificates.py << 'ADMIN_CERT_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from app.admin import bp
from app.admin.utils import admin_required, log_admin_action
from app.models.certificate import Certificate
from app.models.user import User
from app import db
from datetime import datetime, timedelta
from app.utils.qr_generator import generate_certificate_qr
@bp.route('/certificates')
@admin_required
def certificates():
"""Certificate management page"""
log_admin_action("Accessed certificate management")
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
search = request.args.get('search', '')
query = Certificate.query
if status_filter:
query = query.filter(Certificate.status == status_filter)
if search:
query = query.join(User).filter(
db.or_(
Certificate.product_name.contains(search),
Certificate.material_type.contains(search),
User.username.contains(search)
)
)
certificates = query.order_by(
Certificate.created_at.desc()
).paginate(page=page, per_page=25, error_out=False)
# Get statistics
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count()
}
return render_template('admin/certificates/list.html',
certificates=certificates,
stats=stats,
status_filter=status_filter,
search=search)
@bp.route('/certificates/<int:cert_id>')
@admin_required
def certificate_detail(cert_id):
"""Certificate detail page"""
certificate = Certificate.query.get_or_404(cert_id)
log_admin_action("Viewed certificate details", "certificate", cert_id)
return render_template('admin/certificates/detail.html',
certificate=certificate)
@bp.route('/certificates/<int:cert_id>/approve', methods=['POST'])
@admin_required
def approve_certificate(cert_id):
"""Approve certificate"""
certificate = Certificate.query.get_or_404(cert_id)
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'approved'
certificate.approved_at = datetime.utcnow()
certificate.approved_by = current_user.id
# Generate certificate ID and QR code
if not certificate.certificate_id:
certificate.generate_certificate_id()
# Generate QR code
qr_result = generate_certificate_qr(certificate.certificate_id)
certificate.qr_code_path = qr_result['filename']
db.session.commit()
log_admin_action("Approved certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate approved successfully',
'certificate_id': certificate.certificate_id
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/<int:cert_id>/reject', methods=['POST'])
@admin_required
def reject_certificate(cert_id):
"""Reject certificate"""
certificate = Certificate.query.get_or_404(cert_id)
data = request.get_json()
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'rejected'
certificate.rejection_reason = data.get('reason', 'No reason provided')
certificate.rejected_at = datetime.utcnow()
certificate.rejected_by = current_user.id
db.session.commit()
log_admin_action("Rejected certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate rejected successfully'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/<int:cert_id>/revoke', methods=['POST'])
@admin_required
def revoke_certificate(cert_id):
"""Revoke approved certificate"""
certificate = Certificate.query.get_or_404(cert_id)
data = request.get_json()
if certificate.status != 'approved':
return jsonify({
'success': False,
'error': 'Only approved certificates can be revoked'
}), 400
try:
certificate.status = 'revoked'
certificate.revocation_reason = data.get('reason', 'No reason provided')
certificate.revoked_at = datetime.utcnow()
certificate.revoked_by = current_user.id
db.session.commit()
log_admin_action("Revoked certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate revoked successfully'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/pending')
@admin_required
def pending_certificates():
"""Get pending certificates for approval queue"""
certificates = Certificate.query.filter_by(
status='pending'
).order_by(Certificate.created_at.asc()).all()
return jsonify([{
'id': cert.id,
'product_name': cert.product_name,
'material_type': cert.material_type,
'supplier': cert.user.username,
'created_at': cert.created_at.isoformat(),
'days_pending': (datetime.utcnow() - cert.created_at).days
} for cert in certificates])
@bp.route('/certificates/stats')
@admin_required
def certificate_stats():
"""Get certificate statistics"""
from sqlalchemy import func, extract
# Basic stats
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'revoked': Certificate.query.filter_by(status='revoked').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count()
}
# Certificates by month (last 12 months)
monthly_data = db.session.query(
extract('year', Certificate.created_at).label('year'),
extract('month', Certificate.created_at).label('month'),
func.count(Certificate.id).label('count')
).filter(
Certificate.created_at >= datetime.utcnow() - timedelta(days=365)
).group_by('year', 'month').all()
stats['monthly'] = [{
'year': int(row.year),
'month': int(row.month),
'count': row.count
} for row in monthly_data]
# Top material types
material_stats = db.session.query(
Certificate.material_type,
func.count(Certificate.id).label('count')
).group_by(Certificate.material_type).all()
stats['by_material'] = [{
'material': row[0],
'count': row[1]
} for row in material_stats]
return jsonify(stats)
CERT_VIEWS
# 7. Create Blockchain Management Views
cat > backend/app/admin/views/blockchain.py << 'ADMIN_BLOCKCHAIN_VIEWS'
from flask import render_template, request, jsonify, current_app
from app.admin import bp
from app.admin.utils import admin_required, log_admin_action, SystemMonitor
import docker
import subprocess
import os
@bp.route('/blockchain')
@admin_required
def blockchain_status():
"""Blockchain network status page"""
log_admin_action("Accessed blockchain management")
# Get Fabric network status
fabric_status = SystemMonitor.check_fabric_network()
# Get container logs (last 50 lines)
container_logs = {}
try:
client = docker.from_env()
for container_name in fabric_status.keys():
try:
container = client.containers.get(container_name)
logs = container.logs(tail=50).decode('utf-8')
container_logs[container_name] = logs.split('\n')[-50:]
except:
container_logs[container_name] = ['Container not found']
except Exception as e:
current_app.logger.error(f"Error getting container logs: {e}")
return render_template('admin/blockchain/status.html',
fabric_status=fabric_status,
container_logs=container_logs)
@bp.route('/blockchain/start-network', methods=['POST'])
@admin_required
def start_fabric_network():
"""Start Hyperledger Fabric network"""
try:
fabric_path = "/home/michal/NanoTrace/fabric-network"
script_path = os.path.join(fabric_path, "network/scripts/start_network.sh")
if not os.path.exists(script_path):
return jsonify({
'success': False,
'error': 'Fabric network script not found'
}), 400
# Run the start script
result = subprocess.run([script_path],
capture_output=True,
text=True,
timeout=300) # 5 minute timeout
log_admin_action("Started Fabric network")
if result.returncode == 0:
return jsonify({
'success': True,
'message': 'Fabric network started successfully',
'output': result.stdout
})
else:
return jsonify({
'success': False,
'error': f'Failed to start network: {result.stderr}'
}), 500
except subprocess.TimeoutExpired:
return jsonify({
'success': False,
'error': 'Network start timeout (5 minutes)'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error starting network: {str(e)}'
}), 500
@bp.route('/blockchain/stop-network', methods=['POST'])
@admin_required
def stop_fabric_network():
"""Stop Hyperledger Fabric network"""
try:
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com',
'cli'
]
stopped_containers = []
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container.stop()
stopped_containers.append(container_name)
except docker.errors.NotFound:
pass # Container already stopped
log_admin_action("Stopped Fabric network")
return jsonify({
'success': True,
'message': f'Stopped {len(stopped_containers)} containers',
'stopped': stopped_containers
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Error stopping network: {str(e)}'
}), 500
@bp.route('/blockchain/restart-network', methods=['POST'])
@admin_required
def restart_fabric_network():
"""Restart Hyperledger Fabric network"""
try:
# First stop
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com',
'cli'
]
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container.stop()
except docker.errors.NotFound:
pass
# Then start
fabric_path = "/home/michal/NanoTrace/fabric-network"
script_path = os.path.join(fabric_path, "network/scripts/start_network.sh")
result = subprocess.run([script_path],
capture_output=True,
text=True,
timeout=300)
log_admin_action("Restarted Fabric network")
if result.returncode == 0:
return jsonify({
'success': True,
'message': 'Fabric network restarted successfully'
})
else:
return jsonify({
'success': False,
'error': f'Failed to restart network: {result.stderr}'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error restarting network: {str(e)}'
}), 500
@bp.route('/blockchain/container-logs/<container_name>')
@admin_required
def get_container_logs(container_name):
"""Get logs for specific container"""
try:
client = docker.from_env()
container = client.containers.get(container_name)
lines = request.args.get('lines', 100, type=int)
logs = container.logs(tail=lines).decode('utf-8')
return jsonify({
'success': True,
'logs': logs.split('\n'),
'container': container_name
})
except docker.errors.NotFound:
return jsonify({
'success': False,
'error': f'Container {container_name} not found'
}), 404
except Exception as e:
return jsonify({
'success': False,
'error': f'Error getting logs: {str(e)}'
}), 500
@bp.route('/blockchain/chaincode-info')
@admin_required
def chaincode_info():
"""Get chaincode information"""
try:
# This would typically query the Fabric network for chaincode info
# For now, return static info based on project structure
chaincode_path = "/home/michal/NanoTrace/chaincode"
if os.path.exists(chaincode_path):
# Get Go mod info
go_mod_path = os.path.join(chaincode_path, "go.mod")
go_mod_content = ""
if os.path.exists(go_mod_path):
with open(go_mod_path, 'r') as f:
go_mod_content = f.read()
return jsonify({
'success': True,
'chaincode_path': chaincode_path,
'go_mod': go_mod_content,
'status': 'Available (not deployed)'
})
else:
return jsonify({
'success': False,
'error': 'Chaincode directory not found'
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Error getting chaincode info: {str(e)}'
}), 500
ADMIN_BLOCKCHAIN_VIEWS
# 8. Create System Management Views
cat > backend/app/admin/views/system.py << 'ADMIN_SYSTEM_VIEWS'
from flask import render_template, request, jsonify, current_app, send_file
from app.admin import bp
from app.admin.utils import admin_required, super_admin_required, log_admin_action, SystemMonitor, format_bytes
from app.models.admin_models import SystemConfig, SystemMetric
from app import db
from datetime import datetime, timedelta
import os
import subprocess
import tempfile
import json
import shutil
@bp.route('/system')
@admin_required
def system_overview():
"""System overview page"""
log_admin_action("Accessed system management")
system_stats = SystemMonitor.get_system_stats()
# Get system configuration
configs = SystemConfig.query.all()
config_dict = {config.key: config.value for config in configs}
# Get disk usage for important directories
directories = [
'/home/michal/NanoTrace',
'/var/log',
'/tmp',
'/home/michal/NanoTrace/backend/app/static/qr_codes'
]
disk_usage = {}
for directory in directories:
if os.path.exists(directory):
try:
usage = shutil.disk_usage(directory)
disk_usage[directory] = {
'total': usage.total,
'used': usage.used,
'free': usage.free,
'total_formatted': format_bytes(usage.total),
'used_formatted': format_bytes(usage.used),
'free_formatted': format_bytes(usage.free)
}
except:
disk_usage[directory] = None
return render_template('admin/system/overview.html',
system_stats=system_stats,
config_dict=config_dict,
disk_usage=disk_usage)
@bp.route('/system/config')
@admin_required
def system_config():
"""System configuration management"""
configs = SystemConfig.query.order_by(SystemConfig.key).all()
return render_template('admin/system/config.html', configs=configs)
@bp.route('/system/config/update', methods=['POST'])
@admin_required
def update_system_config():
"""Update system configuration"""
data = request.get_json()
key = data.get('key')
value = data.get('value')
description = data.get('description')
if not key:
return jsonify({'success': False, 'error': 'Key is required'}), 400
try:
config = SystemConfig.set_config(key, value, current_user.id, description)
log_admin_action(f"Updated config: {key}", "system_config", key)
return jsonify({
'success': True,
'message': f'Configuration {key} updated successfully'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/system/logs')
@admin_required
def system_logs():
"""View system logs"""
log_type = request.args.get('type', 'flask')
lines = request.args.get('lines', 100, type=int)
log_files = {
'flask': '/var/log/nanotrace/app.log',
'nginx': '/var/log/nginx/access.log',
'nginx_error': '/var/log/nginx/error.log',
'system': '/var/log/syslog',
'postgresql': '/var/log/postgresql/postgresql-16-main.log'
}
log_content = []
error_message = None
if log_type in log_files:
log_file = log_files[log_type]
try:
if os.path.exists(log_file):
# Use tail command to get last N lines
result = subprocess.run(['tail', '-n', str(lines), log_file],
capture_output=True, text=True)
if result.returncode == 0:
log_content = result.stdout.strip().split('\n')
else:
error_message = f"Error reading log: {result.stderr}"
else:
error_message = f"Log file not found: {log_file}"
except Exception as e:
error_message = f"Error accessing log: {str(e)}"
else:
error_message = "Invalid log type"
return render_template('admin/system/logs.html',
log_content=log_content,
log_type=log_type,
error_message=error_message,
available_logs=list(log_files.keys()))
@bp.route('/system/backup', methods=['POST'])
@super_admin_required
def create_backup():
"""Create system backup"""
try:
backup_dir = "/tmp/nanotrace_backup_" + datetime.now().strftime("%Y%m%d_%H%M%S")
os.makedirs(backup_dir, exist_ok=True)
# Database backup
db_backup_file = os.path.join(backup_dir, "database.sql")
db_cmd = ["pg_dump", "-h", "localhost", "-U", "nanotrace", "-d", "nanotrace", "-f", db_backup_file]
subprocess.run(db_cmd, check=True, env={"PGPASSWORD": "password"})
# Application files backup (excluding venv and logs)
app_backup_file = os.path.join(backup_dir, "application.tar.gz")
tar_cmd = ["tar", "-czf", app_backup_file,
"--exclude=venv", "--exclude=*.log", "--exclude=__pycache__",
"-C", "/home/michal", "NanoTrace"]
subprocess.run(tar_cmd, check=True)
# Create backup archive
final_backup = f"/tmp/nanotrace_full_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.tar.gz"
final_cmd = ["tar", "-czf", final_backup, "-C", "/tmp", os.path.basename(backup_dir)]
subprocess.run(final_cmd, check=True)
# Cleanup temp directory
shutil.rmtree(backup_dir)
log_admin_action("Created system backup", "system", final_backup)
return jsonify({
'success': True,
'message': 'Backup created successfully',
'backup_file': final_backup,
'size': format_bytes(os.path.getsize(final_backup))
})
except subprocess.CalledProcessError as e:
return jsonify({
'success': False,
'error': f'Backup command failed: {str(e)}'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Backup failed: {str(e)}'
}), 500
@bp.route('/system/cleanup', methods=['POST'])
@admin_required
def system_cleanup():
"""Clean up temporary files and old data"""
cleaned_items = []
total_freed = 0
try:
# Clean old QR codes (older than 30 days)
from app.utils.qr_generator import QRCodeGenerator
generator = QRCodeGenerator()
qr_cleaned = generator.cleanup_old_qr_codes(30)
if qr_cleaned:
cleaned_items.append(f"Removed {qr_cleaned} old QR codes")
# Clean old metrics (older than 90 days)
cutoff_date = datetime.utcnow() - timedelta(days=90)
old_metrics = SystemMetric.query.filter(SystemMetric.timestamp < cutoff_date)
metrics_count = old_metrics.count()
if metrics_count > 0:
old_metrics.delete()
db.session.commit()
cleaned_items.append(f"Removed {metrics_count} old metrics")
# Clean temp files
temp_dirs = ['/tmp', '/var/tmp']
for temp_dir in temp_dirs:
if os.path.exists(temp_dir):
for item in os.listdir(temp_dir):
if item.startswith('nanotrace_'):
item_path = os.path.join(temp_dir, item)
if os.path.isfile(item_path):
size = os.path.getsize(item_path)
os.remove(item_path)
total_freed += size
elif os.path.isdir(item_path):
size = sum(os.path.getsize(os.path.join(dirpath, filename))
for dirpath, dirnames, filenames in os.walk(item_path)
for filename in filenames)
shutil.rmtree(item_path)
total_freed += size
if total_freed > 0:
cleaned_items.append(f"Freed {format_bytes(total_freed)} from temp files")
log_admin_action("Performed system cleanup")
return jsonify({
'success': True,
'message': 'System cleanup completed',
'cleaned_items': cleaned_items,
'total_freed': format_bytes(total_freed)
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Cleanup failed: {str(e)}'
}), 500
@bp.route('/system/services')
@admin_required
def system_services():
"""View system services status"""
services = [
'postgresql',
'redis-server',
'nginx',
'nanotrace' # Our Flask app service
]
service_status = {}
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
# Get additional info
info_result = subprocess.run(['systemctl', 'show', service,
'--property=LoadState,ActiveState,SubState,MainPID'],
capture_output=True, text=True)
service_info = {}
for line in info_result.stdout.strip().split('\n'):
if '=' in line:
key, value = line.split('=', 1)
service_info[key] = value
service_status[service] = {
'status': status,
'active': status == 'active',
'info': service_info
}
except Exception as e:
service_status[service] = {
'status': 'error',
'active': False,
'error': str(e)
}
return render_template('admin/system/services.html',
services=service_status)
@bp.route('/system/services/<service_name>/<action>', methods=['POST'])
@super_admin_required
def control_service(service_name, action):
"""Control system services"""
allowed_services = ['postgresql', 'redis-server', 'nginx', 'nanotrace']
allowed_actions = ['start', 'stop', 'restart', 'reload']
if service_name not in allowed_services or action not in allowed_actions:
return jsonify({
'success': False,
'error': 'Invalid service or action'
}), 400
try:
result = subprocess.run(['sudo', 'systemctl', action, service_name],
capture_output=True, text=True, timeout=30)
log_admin_action(f"{action.title()} service: {service_name}", "service", service_name)
if result.returncode == 0:
return jsonify({
'success': True,
'message': f'Service {service_name} {action} completed'
})
else:
return jsonify({
'success': False,
'error': f'Service {action} failed: {result.stderr}'
}), 500
except subprocess.TimeoutExpired:
return jsonify({
'success': False,
'error': 'Service operation timed out'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error controlling service: {str(e)}'
}), 500
@bp.route('/system/export-data')
@super_admin_required
def export_system_data():
"""Export system data for analysis"""
try:
# Create temporary file for export
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
export_data = {
'export_date': datetime.utcnow().isoformat(),
'system_stats': SystemMonitor.get_system_stats(),
'database_stats': SystemMonitor.get_database_stats(),
'fabric_status': SystemMonitor.check_fabric_network(),
'recent_metrics': [metric.to_dict() for metric in
SystemMetric.query.order_by(
SystemMetric.timestamp.desc()
).limit(1000).all()],
'recent_audit_logs': [log.to_dict() for log in
AuditLog.query.order_by(
AuditLog.timestamp.desc()
).limit(500).all()]
}
json.dump(export_data, temp_file, indent=2)
temp_file.close()
log_admin_action("Exported system data")
return send_file(
temp_file.name,
as_attachment=True,
download_name=f"nanotrace_system_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
mimetype='application/json'
)
except Exception as e:
return jsonify({
'success': False,
'error': f'Export failed: {str(e)}'
}), 500
ADMIN_SYSTEM_VIEWSChatGPT said:
Awesome—let’s wire this Admin console in cleanly without breaking your current package layout.
Below is a tight, copy-paste plan that adapts your snippets to the backend.* package you’re running under Gunicorn, adds the missing bits, and keeps imports correct.
1) Create folders + install libs
bashcd ~/NanoTrace
source venv/bin/activate
pip install psutil docker
mkdir -p backend/app/admin/{views,templates,static}
mkdir -p backend/app/admin/static/{css,js}
mkdir -p backend/app/admin/templates/{dashboard,users,certificates,blockchain,system}
2) Admin blueprint (use package-relative imports)
backend/app/admin/__init__.py
pythonfrom flask import Blueprint
bp = Blueprint(
"admin",
__name__,
template_folder="templates",
static_folder="static",
static_url_path="/admin/static",
)
# IMPORTANT: import views using package path
from backend.app.admin.views import main, users # noqa: E402,F401
You can add
certificates,blockchain,systemlater as you finish those files—start withmain+usersso the dashboard comes up immediately.
3) Minimal views (work now, expand later)
backend/app/admin/views/main.py
pythonfrom flask import render_template, jsonify
from flask_login import current_user
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, SystemMonitor
@bp.route("/dashboard")
@admin_required
def dashboard():
system_stats = SystemMonitor.get_system_stats()
db_stats = {"total_users": 0, "total_certificates": 0, "pending_certificates": 0}
fabric_status = {}
recent_logs = []
return render_template(
"dashboard/main.html",
system_stats=system_stats,
db_stats=db_stats,
fabric_status=fabric_status,
recent_logs=recent_logs,
)
@bp.route("/api/system-stats")
@admin_required
def api_system_stats():
return jsonify({"system": SystemMonitor.get_system_stats()})
backend/app/admin/views/users.py
pythonfrom flask import render_template, request, jsonify
from backend.app.admin import bp
from backend.app.admin.utils import admin_required
from backend.app import db
from backend.app.models.user import User
@bp.route("/users")
@admin_required
def users():
page = request.args.get("page", 1, type=int)
search = request.args.get("search", "")
role_filter = request.args.get("role", "")
query = User.query
if search:
query = query.filter(db.or_(User.username.ilike(f"%{search}%"),
User.email.ilike(f"%{search}%")))
if role_filter:
query = query.filter(User.role == role_filter)
users = query.paginate(page=page, per_page=25, error_out=False)
return render_template("users/list.html", users=users, search=search, role_filter=role_filter)
@bp.route("/users/<int:user_id>/toggle-status", methods=["POST"])
@admin_required
def toggle_user_status(user_id):
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
db.session.commit()
return jsonify({"success": True, "is_active": user.is_active})
4) Utilities (admin guard + system stats)
backend/app/admin/utils.py
pythonfrom functools import wraps
from flask import abort
from flask_login import current_user
import psutil
def admin_required(f):
@wraps(f)
def w(*a, **k):
if not current_user.is_authenticated or getattr(current_user, "role", "user") not in ("admin", "super_admin"):
abort(403)
return f(*a, **k)
return w
class SystemMonitor:
@staticmethod
def get_system_stats():
try:
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
return {
"cpu": {"percent": psutil.cpu_percent(interval=0.2)},
"memory": {"percent": mem.percent, "used": mem.used, "total": mem.total},
"disk": {"percent": disk.percent, "free": disk.free, "total": disk.total},
}
except Exception:
return {"cpu": {"percent": 0}, "memory": {"percent": 0}, "disk": {"percent": 0}}
5) Templates (drop in what you pasted)
-
Save your
ADMIN_BASE_TEMPLATEas:-
backend/app/admin/templates/admin_base.html
-
-
Save your
DASHBOARD_MAIN_TEMPLATEas:-
backend/app/admin/templates/dashboard/main.html
-
-
Save your
USER_LIST_TEMPLATEas:-
backend/app/admin/templates/users/list.html
-
-
Save your admin CSS:
-
backend/app/admin/static/css/admin.css
-
6) Register the blueprint (without breaking imports)
Open backend/app/__init__.py and ensure:
-
It imports config as
from backend.config.config import Config -
It registers blueprints using relative package imports.
Add the admin blueprint registration inside create_app():
python# after other blueprints
from backend.app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix="/admin")
7) Model fields (role / status) + migration
If your User and Certificate models don’t yet have the admin fields the templates expect, add these:
User (backend/app/models/user.py):
pythonfrom datetime import datetime
from backend.app import db
# below existing columns
role = db.Column(db.String(20), default="user", nullable=False)
is_verified = db.Column(db.Boolean, default=False, nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
last_login = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
Certificate (backend/app/models/certificate.py):
pythonfrom backend.app import db
from datetime import datetime
status = db.Column(db.String(20), default="pending", nullable=False)
approved_at = db.Column(db.DateTime)
approved_by = db.Column(db.Integer, db.ForeignKey("users.id"))
rejected_at = db.Column(db.DateTime)
rejected_by = db.Column(db.Integer, db.ForeignKey("users.id"))
rejection_reason = db.Column(db.Text)
revoked_at = db.Column(db.DateTime)
revoked_by = db.Column(db.Integer, db.ForeignKey("users.id"))
revocation_reason = db.Column(db.Text)
qr_code_path = db.Column(db.String(255))
Run migrations:
bashcd ~/NanoTrace
source venv/bin/activate
export FLASK_APP=backend.app:create_app
flask db migrate -m "admin fields"
flask db upgrade
8) Quick sanity checks (dev)
bash# run quickly in dev to verify routes
source venv/bin/activate
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("Loaded:", app)
with app.test_client() as c:
r = c.get("/admin/dashboard", follow_redirects=False)
print("/admin/dashboard status:", r.status_code)
PY
If you get 302 (redirect to login), that’s correct—admin is protected.
9) Gunicorn + NGINX (no changes needed)
Your Gunicorn/NGINX are already up. Just restart the app to pick up the new code:
bashsudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
10) Create an admin user (one-time)
If you don’t already have an admin, promote your first user in a Python shell:
bashsource venv/bin/activate
python - <<'PY'
from backend.wsgi import app
from backend.app import db
from backend.app.models.user import User
with app.app_context():
u = User.query.first()
if u:
u.role = "super_admin"
u.is_verified = True
db.session.commit()
print("Promoted:", u.username)
else:
print("No users yet.")
PY
Notes about your pasted script
-
Many lines used
from app...imports. In your project, the correct path isbackend.app...(or relative imports inside that package). I’ve already adjusted those in the minimal files above. If you bring in the rest (blockchain/system/certificates views, monitoring, cron etc.), replace anyfrom app ...withfrom backend.app .... -
Start small: get
/admin/dashboardand/admin/usersworking first (they’re included above). Then add the rest of your views/templates in chunks. -
Your
admin_base.htmlpulls Tailwind via CDN—no extra build steps needed.
If you want, I can also give you quick sed lines to mass-fix lingering from app → from backend.app in any new files you add.
You said:
DASHBOARD_MAIN_TEMPLATE
# 10. Create additional admin templates and finalize setup
# User list template
cat > backend/app/admin/templates/users/list.html << 'USER_LIST_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}User Management{% endblock %}
{% block content %}
<!-- Header with search and filters -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">User Management</h2>
<div class="flex flex-col sm:flex-row gap-4">
<!-- Search -->
<div class="relative">
<input type="text" id="user-search" placeholder="Search users..."
value="{{ search }}"
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<!-- Role filter -->
<select id="role-filter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Roles</option>
<option value="user" {{ 'selected' if role_filter == 'user' }}>User</option>
<option value="admin" {{ 'selected' if role_filter == 'admin' }}>Admin</option>
<option value="super_admin" {{ 'selected' if role_filter == 'super_admin' }}>Super Admin</option>
</select>
<!-- Create user button -->
<a href="{{ url_for('admin.create_user') }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-plus mr-2"></i>Create User
</a>
</div>
</div>
</div>
<!-- Users table -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Certificates</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for user in users.items %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
{{ user.username[0].upper() }}
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.username }}</div>
<div class="text-sm text-gray-500">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-purple-100 text-purple-800' if user.role == 'super_admin'
else 'bg-blue-100 text-blue-800' if user.role == 'admin'
else 'bg-gray-100 text-gray-800' }}">
{{ user.role.replace('_', ' ').title() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if user.is_active else 'error' }}"></span>
<span class="text-sm text-gray-900">
{{ 'Active' if user.is_active else 'Inactive' }}
{% if user.is_verified %}
<i class="fas fa-check-circle text-green-500 ml-1" title="Verified"></i>
{% endif %}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ user.certificates.count() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="text-blue-600 hover:text-blue-900">View</a>
<a href="{{ url_for('admin.edit_user', user_id=user.id) }}"
class="text-indigo-600 hover:text-indigo-900">Edit</a>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | lower }})"
class="text-{{ 'red' if user.is_active else 'green' }}-600 hover:text-{{ 'red' if user.is_active else 'green' }}-900">
{{ 'Deactivate' if user.is_active else 'Activate' }}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if users.pages > 1 %}
<div class="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
<div class="flex items-center justify-between">
<div class="flex-1 flex justify-between sm:hidden">
{% if users.has_prev %}
<a href="{{ url_for('admin.users', page=users.prev_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Previous</a>
{% endif %}
{% if users.has_next %}
<a href="{{ url_for('admin.users', page=users.next_num, search=search, role=role_filter) }}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Next</a>
{% endif %}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing <span class="font-medium">{{ users.per_page * (users.page - 1) + 1 }}</span>
to <span class="font-medium">{{ users.per_page * users.page if users.page < users.pages else users.total }}</span>
of <span class="font-medium">{{ users.total }}</span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{% if users.has_prev %}
<a href="{{ url_for('admin.users', page=users.prev_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<i class="fas fa-chevron-left"></i>
</a>
{% endif %}
{% for page_num in users.iter_pages() %}
{% if page_num %}
{% if page_num != users.page %}
<a href="{{ url_for('admin.users', page=page_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
{{ page_num }}
</a>
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">
{{ page_num }}
</span>
{% endif %}
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>
{% endif %}
{% endfor %}
{% if users.has_next %}
<a href="{{ url_for('admin.users', page=users.next_num, search=search, role=role_filter) }}"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<i class="fas fa-chevron-right"></i>
</a>
{% endif %}
</nav>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
// Search functionality
document.getElementById('user-search').addEventListener('input', function(e) {
const search = e.target.value;
const roleFilter = document.getElementById('role-filter').value;
updateUserFilter(search, roleFilter);
});
document.getElementById('role-filter').addEventListener('change', function(e) {
const roleFilter = e.target.value;
const search = document.getElementById('user-search').value;
updateUserFilter(search, roleFilter);
});
function updateUserFilter(search, role) {
const url = new URL(window.location);
url.searchParams.set('search', search);
url.searchParams.set('role', role);
url.searchParams.set('page', '1'); // Reset to first page
window.location.href = url.toString();
}
function toggleUserStatus(userId, isActive) {
const action = isActive ? 'deactivate' : 'activate';
if (confirm(
Are you sure you want to ${action} this user?)) {
showLoading();
fetch(/admin/users/${userId}/toggle-status, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
USER_LIST_TEMPLATE
# 11. Create Admin CSS and final integration scripts
cat > backend/app/admin/static/css/admin.css << 'ADMIN_CSS'
/* Custom admin dashboard styles */
.admin-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.metric-card {
transition: all 0.3s ease;
}
.metric-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.status-card {
border-left: 4px solid;
}
.status-card.healthy {
border-color: #10b981;
}
.status-card.warning {
border-color: #f59e0b;
}
.status-card.error {
border-color: #ef4444;
}
/* Loading animations */
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
/* Table enhancements */
.admin-table tbody tr:hover {
background-color: #f8fafc;
}
/* Chart containers */
.chart-container {
position: relative;
height: 300px;
}
/* Custom scrollbar */
.admin-scroll::-webkit-scrollbar {
width: 6px;
}
.admin-scroll::-webkit-scrollbar-track {
background: #f1f5f9;
}
.admin-scroll::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.admin-scroll::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Mobile responsive fixes */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
}
ADMIN_CSS
# 12. Create admin integration script
cat > integrate_admin.sh << 'INTEGRATE_ADMIN'
#!/bin/bash
set -e
echo "🔗 Integrating Admin Dashboard with Flask App..."
cd $PROJECT_ROOT
source venv/bin/activate
# Update main Flask app to register admin blueprint
cat >> backend/app/__init__.py << 'FLASK_ADMIN_INTEGRATION'
# Register admin blueprint
from app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
# Import admin models for database creation
from app.models import admin_models
FLASK_ADMIN_INTEGRATION
# Update User model to include admin functionality
cat >> backend/app/models/user.py << 'USER_ADMIN_FIELDS'
# Admin fields
role = db.Column(db.String(20), default='user')
is_verified = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True)
last_login = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@property
def is_admin(self):
return self.role in ['admin', 'super_admin']
def has_role(self, role):
return self.role == role
USER_ADMIN_FIELDS
# Update Certificate model with admin fields
cat >> backend/app/models/certificate.py << 'CERT_ADMIN_FIELDS'
# Admin approval fields
status = db.Column(db.String(20), default='pending') # pending, approved, rejected, revoked
approved_at = db.Column(db.DateTime)
approved_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejected_at = db.Column(db.DateTime)
rejected_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejection_reason = db.Column(db.Text)
revoked_at = db.Column(db.DateTime)
revoked_by = db.Column(db.Integer, db.ForeignKey('users.id'))
revocation_reason = db.Column(db.Text)
# QR Code path
qr_code_path = db.Column(db.String(255))
# Relationships
approver = db.relationship('User', foreign_keys=[approved_by], backref='approved_certificates')
rejecter = db.relationship('User', foreign_keys=[rejected_by], backref='rejected_certificates')
revoker = db.relationship('User', foreign_keys=[revoked_by], backref='revoked_certificates')
def generate_certificate_id(self):
"""Generate unique certificate ID"""
import uuid
import hashlib
unique_string = f"{self.product_name}_{self.user_id}_{uuid.uuid4().hex[:8]}"
self.certificate_id = hashlib.md5(unique_string.encode()).hexdigest()[:12].upper()
return self.certificate_id
CERT_ADMIN_FIELDS
# Create database migration for admin fields
echo "📊 Creating database migration for admin dashboard..."
flask db migrate -m "Add admin dashboard fields"
flask db upgrade
# Create default admin user
cat > scripts/create_admin.py << 'CREATE_ADMIN_SCRIPT'
#!/usr/bin/env python3
"""Create default admin user for NanoTrace"""
import sys
import os
sys.path.append('/home/michal/NanoTrace/backend')
from app import create_app, db
from app.models.user import User
import getpass
def create_admin():
app = create_app()
with app.app_context():
print("Creating NanoTrace Admin User")
print("=" * 30)
username = input("Admin username: ")
email = input("Admin email: ")
password = getpass.getpass("Admin password: ")
# Check if user already exists
existing_user = User.query.filter_by(username=username).first()
if existing_user:
print(f"User {username} already exists!")
return
existing_email = User.query.filter_by(email=email).first()
if existing_email:
print(f"Email {email} already exists!")
return
# Create admin user
admin_user = User(
username=username,
email=email,
role='super_admin',
is_verified=True,
is_active=True
)
admin_user.set_password(password)
db.session.add(admin_user)
db.session.commit()
print(f"✅ Admin user {username} created successfully!")
print(f"You can now login at: http://your-domain/admin/dashboard")
if __name__ == '__main__':
create_admin()
CREATE_ADMIN_SCRIPT
chmod +x scripts/create_admin.py
# Create systemd service monitoring script
cat > scripts/monitor_services.py << 'SERVICE_MONITOR_SCRIPT'
#!/usr/bin/env python3
"""Monitor system services and record metrics"""
import sys
import os
import time
import subprocess
from datetime import datetime
# Add backend to path
sys.path.append('/home/michal/NanoTrace/backend')
from app import create_app, db
from app.admin.utils import SystemMonitor
def monitor_services():
"""Monitor services and record metrics"""
app = create_app()
with app.app_context():
try:
# Record system metrics
SystemMonitor.record_metrics()
# Check services
services = ['postgresql', 'redis-server', 'nginx', 'nanotrace']
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
print(f"{datetime.now()}: {service} - {status}")
except Exception as e:
print(f"{datetime.now()}: Error checking {service}: {e}")
print(f"{datetime.now()}: Monitoring cycle completed")
except Exception as e:
print(f"{datetime.now()}: Error in monitoring: {e}")
if __name__ == '__main__':
monitor_services()
SERVICE_MONITOR_SCRIPT
chmod +x scripts/monitor_services.py
# Create cron job for monitoring
echo "⏰ Setting up monitoring cron job..."
(crontab -l 2>/dev/null; echo "*/5 * * * * cd /home/michal/NanoTrace && /home/michal/NanoTrace/venv/bin/python scripts/monitor_services.py >> /var/log/nanotrace_monitor.log 2>&1") | crontab -
echo "✅ Admin Dashboard Integration Complete!"
echo ""
echo "Next steps:"
echo "1. Run: python scripts/create_admin.py (to create admin user)"
echo "2. Access admin dashboard at: http://your-domain/admin/dashboard"
echo "3. Monitor logs at: tail -f /var/log/nanotrace_monitor.log"
echo ""
echo "Admin Features Available:"
echo " 📊 System monitoring with real-time metrics"
echo " 👥 User management (create, edit, activate/deactivate)"
echo " 📄 Certificate approval workflow"
echo " 🔗 Blockchain network control"
echo " ⚙️ System configuration management"
echo " 📋 Audit logging and activity tracking"
echo " 💾 Backup and cleanup utilities"
echo " 🔧 Service control and log viewing"
INTEGRATE_ADMIN
chmod +x integrate_admin.sh
echo "🎛️ Admin Dashboard Builder Complete!"
echo ""
echo "Generated Files:"
echo " 📁 backend/app/admin/ - Admin blueprint with views and templates"
echo " 📊 backend/app/models/admin_models.py - Admin data models"
echo " 🎨 backend/app/admin/templates/ - Professional admin UI templates"
echo " 🔧 scripts/create_admin.py - Admin user creation script"
echo " 📈 scripts/monitor_services.py - System monitoring script"
echo " 🔗 integrate_admin.sh - Integration script"
echo ""
echo "To complete setup, run:"
echo " ./integrate_admin.sh"
echo ""
echo "Admin Dashboard Features:"
echo " 🎯 Real-time system monitoring with charts"
echo " 👤 Complete user management system"
echo " 📋 Certificate approval workflow"
echo " ⛓️ Blockchain network management"
echo " 🔧 System configuration and maintenance"
echo " 📊 Analytics and reporting"
echo " 🔒 Audit logging and security"
echo " 💾 Backup and cleanup tools"
echo ""
echo "The dashboard provides a professional admin interface with:"
echo " • Modern responsive design with Tailwind CSS"
echo " • Real-time metrics and charts"
echo " • Role-based access control"
echo " • Comprehensive system management"
echo " • Mobile-friendly interface"
echo " • Dark/light theme support" }), 500
ADMIN_SYSTEM_VIEWS
# 9. Create Admin Dashboard Templates
# Base template
cat > backend/app/admin/templates/admin_base.html << 'ADMIN_BASE_TEMPLATE'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}NanoTrace Admin Dashboard{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.sidebar-transition {
transition: all 0.3s ease-in-out;
}
.admin-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-healthy { background-color: #10b981; }
.status-warning { background-color: #f59e0b; }
.status-error { background-color: #ef4444; }
.metric-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
</style>
</head>
<body class="bg-gray-100">
<div class="flex min-h-screen">
<!-- Sidebar -->
<aside class="bg-gray-900 text-white w-64 min-h-screen sidebar-transition" id="sidebar">
<div class="p-4">
<div class="flex items-center mb-8">
<i class="fas fa-cog text-2xl mr-3 text-blue-400"></i>
<h1 class="text-xl font-bold">Admin Panel</h1>
</div>
<nav class="space-y-2">
<a href="{{ url_for('admin.dashboard') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-tachometer-alt mr-3"></i>
Dashboard
</a>
<a href="{{ url_for('admin.users') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-users mr-3"></i>
Users
</a>
<a href="{{ url_for('admin.certificates') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-certificate mr-3"></i>
Certificates
</a>
<a href="{{ url_for('admin.blockchain_status') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-link mr-3"></i>
Blockchain
</a>
<a href="{{ url_for('admin.system_overview') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-server mr-3"></i>
System
</a>
<hr class="my-4 border-gray-700">
<a href="{{ url_for('main.index') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-home mr-3"></i>
Back to Site
</a>
<a href="{{ url_for('auth.logout') }}"
class="flex items-center px-4 py-2 text-red-400 hover:bg-red-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-sign-out-alt mr-3"></i>
Logout
</a>
</nav>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-hidden">
<!-- Top Header -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex items-center">
<button id="sidebar-toggle" class="text-gray-500 hover:text-gray-700 mr-4">
<i class="fas fa-bars"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">{% block page_title %}Dashboard{% endblock %}</h2>
</div>
<div class="flex items-center space-x-4">
<!-- System Status Indicator -->
<div class="flex items-center">
<span class="status-indicator status-healthy" id="system-status"></span>
<span class="text-sm text-gray-600">System Status</span>
</div>
<!-- User Info -->
<div class="flex items-center">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium mr-2">
{{ current_user.username[0].upper() }}
</div>
<span class="text-sm text-gray-700">{{ current_user.username }}</span>
</div>
</div>
</div>
</header>
<!-- Page Content -->
<div class="p-6 overflow-y-auto" style="height: calc(100vh - 73px);">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mb-4">
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} bg-{{ 'green' if category == 'success' else 'red' }}-100 border border-{{ 'green' if category == 'success' else 'red' }}-400 text-{{ 'green' if category == 'success' else 'red' }}-700 px-4 py-3 rounded mb-2">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 flex items-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<span>Loading...</span>
</div>
</div>
<!-- Scripts -->
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', function() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-translate-x-full');
});
// System status check
function checkSystemStatus() {
fetch('/admin/api/system-stats')
.then(response => response.json())
.then(data => {
const indicator = document.getElementById('system-status');
// Simple health check based on CPU and memory
const cpu = data.system.cpu?.percent || 0;
const memory = data.system.memory?.percent || 0;
if (cpu > 90 || memory > 90) {
indicator.className = 'status-indicator status-error';
} else if (cpu > 70 || memory > 70) {
indicator.className = 'status-indicator status-warning';
} else {
indicator.className = 'status-indicator status-healthy';
}
})
.catch(() => {
document.getElementById('system-status').className = 'status-indicator status-error';
});
}
// Check status every 30 seconds
checkSystemStatus();
setInterval(checkSystemStatus, 30000);
// Loading overlay functions
function showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
// Auto-hide alerts after 5 seconds
document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
alert.style.opacity = '0';
setTimeout(() => alert.remove(), 300);
}, 5000);
});
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>
ADMIN_BASE_TEMPLATE
# Main dashboard template
cat > backend/app/admin/templates/dashboard/main.html << 'DASHBOARD_MAIN_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<!-- Quick Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- System Health -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-heartbeat text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">System Health</p>
<p class="text-2xl font-bold text-gray-900">
{% if system_stats.cpu.percent < 80 %}Healthy{% else %}Warning{% endif %}
</p>
</div>
</div>
</div>
<!-- Total Users -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<i class="fas fa-users text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Users</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_users or 0 }}</p>
</div>
</div>
</div>
<!-- Certificates -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
<i class="fas fa-certificate text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Certificates</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_certificates or 0 }}</p>
<p class="text-xs text-gray-500">{{ db_stats.pending_certificates or 0 }} pending</p>
</div>
</div>
</div>
<!-- Blockchain Status -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<i class="fas fa-link text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Blockchain</p>
{% set healthy_containers = fabric_status.values() | selectattr('healthy') | list | length %}
<p class="text-2xl font-bold text-gray-900">
{{ healthy_containers }}/{{ fabric_status | length }}
</p>
<p class="text-xs text-gray-500">containers running</p>
</div>
</div>
</div>
</div>
<!-- System Overview -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- System Resources -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">System Resources</h3>
<button id="refresh-stats" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="space-y-4">
<!-- CPU Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>CPU Usage</span>
<span>{{ "%.1f" | format(system_stats.cpu.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ system_stats.cpu.percent }}%"></div>
</div>
</div>
<!-- Memory Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Memory Usage</span>
<span>{{ "%.1f" | format(system_stats.memory.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full" style="width: {{ system_stats.memory.percent }}%"></div>
</div>
</div>
<!-- Disk Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Disk Usage</span>
<span>{{ "%.1f" | format(system_stats.disk.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-orange-600 h-2 rounded-full" style="width: {{ system_stats.disk.percent }}%"></div>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-600">Memory:</span>
<span class="font-medium">{{ "%.1f" | format(system_stats.memory.used / (1024**3)) }} GB used</span>
</div>
<div>
<span class="text-gray-600">Disk:</span>
<span class="font-medium">{{ "%.1f" | format(system_stats.disk.free / (1024**3)) }} GB free</span>
</div>
</div>
</div>
</div>
<!-- Fabric Network Status -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Blockchain Network</h3>
<div class="flex space-x-2">
<button id="start-network" class="px-3 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600">
Start
</button>
<button id="stop-network" class="px-3 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600">
Stop
</button>
</div>
</div>
<div class="space-y-3">
{% for container_name, status in fabric_status.items() %}
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if status.healthy else 'error' }}"></span>
<span class="text-sm font-medium text-gray-700">{{ container_name }}</span>
</div>
<span class="text-xs px-2 py-1 rounded-full {{ 'bg-green-100 text-green-800' if status.healthy else 'bg-red-100 text-red-800' }}">
{{ status.status }}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- CPU Usage Chart -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">CPU Usage (24h)</h3>
<canvas id="cpu-chart" height="300"></canvas>
</div>
<!-- Memory Usage Chart -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Memory Usage (24h)</h3>
<canvas id="memory-chart" height="300"></canvas>
</div>
</div>
<!-- Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Pending Certificates -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Pending Certificates</h3>
<a href="{{ url_for('admin.certificates', status='pending') }}"
class="text-sm text-blue-600 hover:text-blue-800">View All</a>
</div>
<div id="pending-certificates" class="space-y-3">
<div class="text-center text-gray-500 py-4">
<i class="fas fa-spinner fa-spin"></i>
Loading...
</div>
</div>
</div>
<!-- Recent Audit Logs -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Activity</h3>
</div>
<div class="space-y-3">
{% for log in recent_logs[:10] %}
<div class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fas fa-user text-blue-600 text-xs"></i>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-900">
<span class="font-medium">{{ log.user.username if log.user else 'System' }}</span>
{{ log.action }}
</p>
<p class="text-xs text-gray-500">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load charts
loadCPUChart();
loadMemoryChart();
loadPendingCertificates();
// Network control buttons
document.getElementById('start-network').addEventListener('click', function() {
controlFabricNetwork('start');
});
document.getElementById('stop-network').addEventListener('click', function() {
controlFabricNetwork('stop');
});
// Refresh stats button
document.getElementById('refresh-stats').addEventListener('click', function() {
location.reload();
});
});
function loadCPUChart() {
fetch('/admin/api/metrics?metric=cpu_percent&hours=24')
.then(response => response.json())
.then(data => {
const ctx = document.getElementById('cpu-chart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => new Date(d.timestamp).toLocaleTimeString()),
datasets: [{
label: 'CPU %',
data: data.map(d => d.value),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
});
}
function loadMemoryChart() {
fetch('/admin/api/metrics?metric=memory_percent&hours=24')
.then(response => response.json())
.then(data => {
const ctx = document.getElementById('memory-chart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => new Date(d.timestamp).toLocaleTimeString()),
datasets: [{
label: 'Memory %',
data: data.map(d => d.value),
borderColor: 'rgb(16, 185, 129)',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
});
}
function loadPendingCertificates() {
fetch('/admin/certificates/pending')
.then(response => response.json())
.then(data => {
const container = document.getElementById('pending-certificates');
if (data.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-4">No pending certificates</div>';
return;
}
container.innerHTML = data.slice(0, 5).map(cert =>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p class="text-sm font-medium text-gray-900">${cert.product_name}</p>
<p class="text-xs text-gray-500">by ${cert.supplier} • ${cert.days_pending} days ago</p>
</div>
<a href="/admin/certificates/${cert.id}"
class="text-blue-600 hover:text-blue-800 text-sm">Review</a>
</div>
).join('');
});
}
function controlFabricNetwork(action) {
showLoading();
fetch(/admin/blockchain/${action}-network, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert(Network ${action} completed successfully);
setTimeout(() => location.reload(), 2000);
} else {
alert(Failed to ${action} network: ${data.error});
}
})
.catch(error => {
hideLoading();
alert(Error: ${error.message});
});
}
</script>
{% endblock %}
DASHBOARD_MAIN_TEMPLATE#!/bin/bash
# =============================================================================
# NanoTrace Admin Dashboard Builder
# Creates a sophisticated Flask admin dashboard for system management
# =============================================================================
set -e
echo "🎛️ Building NanoTrace Admin Dashboard..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# Create admin blueprint structure
mkdir -p backend/app/admin/{views,templates,static}
mkdir -p backend/app/admin/static/{css,js}
mkdir -p backend/app/admin/templates/{base,dashboard,users,certificates,blockchain,system}
# 1. Create Admin Blueprint
cat > backend/app/admin/__init__.py << 'ADMIN_INIT'
from flask import Blueprint
bp = Blueprint('admin', __name__,
template_folder='templates',
static_folder='static',
static_url_path='/admin/static')
from app.admin.views import main, users, certificates, blockchain, system
ADMIN_INIT
# 2. Create Admin Models
cat > backend/app/models/admin_models.py << 'ADMIN_MODELS'
from datetime import datetime
from app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import enum
class UserRole(enum.Enum):
USER = "user"
ADMIN = "admin"
SUPER_ADMIN = "super_admin"
class SystemMetric(db.Model):
"""Track system metrics and health"""
__tablename__ = 'system_metrics'
id = db.Column(db.Integer, primary_key=True)
metric_name = db.Column(db.String(100), nullable=False)
metric_value = db.Column(db.Float, nullable=False)
metric_unit = db.Column(db.String(20))
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.metric_name,
'value': self.metric_value,
'unit': self.metric_unit,
'timestamp': self.timestamp.isoformat()
}
class AuditLog(db.Model):
"""Track admin actions for security"""
__tablename__ = 'audit_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
action = db.Column(db.String(200), nullable=False)
resource = db.Column(db.String(100))
resource_id = db.Column(db.String(100))
ip_address = db.Column(db.String(45))
user_agent = db.Column(db.Text)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref=db.backref('audit_logs', lazy=True))
def to_dict(self):
return {
'id': self.id,
'user': self.user.username if self.user else 'Unknown',
'action': self.action,
'resource': self.resource,
'resource_id': self.resource_id,
'ip': self.ip_address,
'timestamp': self.timestamp.isoformat()
}
class SystemConfig(db.Model):
"""Store system configuration settings"""
__tablename__ = 'system_config'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.Text)
description = db.Column(db.Text)
updated_by = db.Column(db.Integer, db.ForeignKey('users.id'))
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
updater = db.relationship('User')
@staticmethod
def get_config(key, default=None):
config = SystemConfig.query.filter_by(key=key).first()
return config.value if config else default
@staticmethod
def set_config(key, value, user_id, description=None):
config = SystemConfig.query.filter_by(key=key).first()
if config:
config.value = value
config.updated_by = user_id
config.updated_at = datetime.utcnow()
else:
config = SystemConfig(
key=key,
value=value,
description=description,
updated_by=user_id
)
db.session.add(config)
db.session.commit()
return config
ADMIN_MODELS
# 3. Create Admin Utils
cat > backend/app/admin/utils.py << 'ADMIN_UTILS'
from functools import wraps
from flask import abort, request, current_app
from flask_login import current_user
import psutil
import time
import requests
from datetime import datetime, timedelta
from app.models.admin_models import SystemMetric, AuditLog
from app import db
def admin_required(f):
"""Decorator to require admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role not in ['admin', 'super_admin']:
abort(403)
return f(*args, **kwargs)
return decorated_function
def super_admin_required(f):
"""Decorator to require super admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != 'super_admin':
abort(403)
return f(*args, **kwargs)
return decorated_function
def log_admin_action(action, resource=None, resource_id=None):
"""Log admin actions for audit trail"""
try:
log_entry = AuditLog(
user_id=current_user.id,
action=action,
resource=resource,
resource_id=str(resource_id) if resource_id else None,
ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent', '')[:500]
)
db.session.add(log_entry)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Failed to log admin action: {e}")
class SystemMonitor:
"""System monitoring utilities"""
@staticmethod
def get_system_stats():
"""Get current system statistics"""
try:
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
# Network stats (simplified)
network = psutil.net_io_counters()
return {
'cpu': {
'percent': cpu_percent,
'count': psutil.cpu_count()
},
'memory': {
'total': memory.total,
'available': memory.available,
'percent': memory.percent,
'used': memory.used
},
'disk': {
'total': disk.total,
'used': disk.used,
'free': disk.free,
'percent': (disk.used / disk.total) * 100
},
'network': {
'bytes_sent': network.bytes_sent,
'bytes_recv': network.bytes_recv
},
'timestamp': datetime.utcnow().isoformat()
}
except Exception as e:
current_app.logger.error(f"Error getting system stats: {e}")
return {}
@staticmethod
def record_metrics():
"""Record system metrics to database"""
stats = SystemMonitor.get_system_stats()
if not stats:
return
try:
metrics = [
SystemMetric(metric_name='cpu_percent', metric_value=stats['cpu']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_percent', metric_value=stats['memory']['percent'], metric_unit='%'),
SystemMetric(metric_name='disk_percent', metric_value=stats['disk']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_used', metric_value=stats['memory']['used'], metric_unit='bytes'),
SystemMetric(metric_name='disk_used', metric_value=stats['disk']['used'], metric_unit='bytes'),
]
for metric in metrics:
db.session.add(metric)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Error recording metrics: {e}")
db.session.rollback()
@staticmethod
def check_fabric_network():
"""Check Hyperledger Fabric network status"""
try:
# Check if Fabric containers are running
import docker
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com'
]
container_status = {}
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container_status[container_name] = {
'status': container.status,
'healthy': container.status == 'running'
}
except:
container_status[container_name] = {
'status': 'not_found',
'healthy': False
}
return container_status
except Exception as e:
current_app.logger.error(f"Error checking Fabric network: {e}")
return {}
@staticmethod
def get_database_stats():
"""Get database statistics"""
try:
from app.models.user import User
from app.models.certificate import Certificate
stats = {
'total_users': User.query.count(),
'admin_users': User.query.filter_by(role='admin').count(),
'total_certificates': Certificate.query.count(),
'pending_certificates': Certificate.query.filter_by(status='pending').count(),
'approved_certificates': Certificate.query.filter_by(status='approved').count(),
'rejected_certificates': Certificate.query.filter_by(status='rejected').count(),
}
return stats
except Exception as e:
current_app.logger.error(f"Error getting database stats: {e}")
return {}
def format_bytes(bytes):
"""Format bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes < 1024.0:
return f"{bytes:.1f} {unit}"
bytes /= 1024.0
return f"{bytes:.1f} PB"
def format_uptime(seconds):
"""Format uptime in human readable format"""
days = seconds // 86400
hours = (seconds % 86400) // 3600
minutes = (seconds % 3600) // 60
parts = []
if days:
parts.append(f"{int(days)}d")
if hours:
parts.append(f"{int(hours)}h")
if minutes:
parts.append(f"{int(minutes)}m")
return " ".join(parts) if parts else "0m"
ADMIN_UTILS
# 4. Create Main Admin Views
cat > backend/app/admin/views/main.py << 'ADMIN_MAIN_VIEWS'
from flask import render_template, jsonify, request, current_app
from app.admin import bp
from app.admin.utils import admin_required, SystemMonitor, log_admin_action
from app.models.admin_models import SystemMetric, AuditLog
from datetime import datetime, timedelta
from sqlalchemy import func
@bp.route('/dashboard')
@admin_required
def dashboard():
"""Main admin dashboard"""
log_admin_action("Accessed admin dashboard")
# Get system stats
system_stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
fabric_status = SystemMonitor.check_fabric_network()
# Get recent metrics for charts (last 24 hours)
since = datetime.utcnow() - timedelta(hours=24)
recent_metrics = SystemMetric.query.filter(
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.desc()).limit(100).all()
# Get recent audit logs
recent_logs = AuditLog.query.order_by(
AuditLog.timestamp.desc()
).limit(20).all()
return render_template('admin/dashboard/main.html',
system_stats=system_stats,
db_stats=db_stats,
fabric_status=fabric_status,
recent_metrics=recent_metrics,
recent_logs=recent_logs)
@bp.route('/api/system-stats')
@admin_required
def api_system_stats():
"""API endpoint for real-time system stats"""
stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
return jsonify({
'system': stats,
'database': db_stats,
'timestamp': datetime.utcnow().isoformat()
})
@bp.route('/api/metrics')
@admin_required
def api_metrics():
"""API endpoint for metrics data"""
hours = request.args.get('hours', 24, type=int)
metric_name = request.args.get('metric', 'cpu_percent')
since = datetime.utcnow() - timedelta(hours=hours)
metrics = SystemMetric.query.filter(
SystemMetric.metric_name == metric_name,
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.asc()).all()
data = [{
'timestamp': metric.timestamp.isoformat(),
'value': metric.metric_value
} for metric in metrics]
return jsonify(data)
@bp.route('/api/audit-logs')
@admin_required
def api_audit_logs():
"""API endpoint for audit logs"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
logs = AuditLog.query.order_by(
AuditLog.timestamp.desc()
).paginate(page=page, per_page=per_page, error_out=False)
return jsonify({
'logs': [log.to_dict() for log in logs.items],
'total': logs.total,
'pages': logs.pages,
'current_page': page
})
@bp.route('/health-check')
@admin_required
def health_check():
"""Comprehensive system health check"""
log_admin_action("Performed health check")
health_status = {
'timestamp': datetime.utcnow().isoformat(),
'overall': 'healthy',
'checks': {}
}
# Database check
try:
from app import db
db.session.execute('SELECT 1').fetchone()
health_status['checks']['database'] = {
'status': 'healthy',
'message': 'Database connection successful'
}
except Exception as e:
health_status['checks']['database'] = {
'status': 'unhealthy',
'message': f'Database error: {str(e)}'
}
health_status['overall'] = 'degraded'
# Redis check (if configured)
try:
import redis
redis_client = redis.from_url(current_app.config.get('REDIS_URL', 'redis://localhost:6379'))
redis_client.ping()
health_status['checks']['redis'] = {
'status': 'healthy',
'message': 'Redis connection successful'
}
except Exception as e:
health_status['checks']['redis'] = {
'status': 'unhealthy',
'message': f'Redis error: {str(e)}'
}
health_status['overall'] = 'degraded'
# Fabric network check
fabric_status = SystemMonitor.check_fabric_network()
all_healthy = all(container['healthy'] for container in fabric_status.values())
health_status['checks']['fabric'] = {
'status': 'healthy' if all_healthy else 'unhealthy',
'message': f'Fabric containers: {len([c for c in fabric_status.values() if c["healthy"]])}/{len(fabric_status)} healthy',
'details': fabric_status
}
if not all_healthy:
health_status['overall'] = 'degraded'
# System resources check
system_stats = SystemMonitor.get_system_stats()
cpu_ok = system_stats.get('cpu', {}).get('percent', 0) < 90
memory_ok = system_stats.get('memory', {}).get('percent', 0) < 90
disk_ok = system_stats.get('disk', {}).get('percent', 0) < 90
health_status['checks']['system_resources'] = {
'status': 'healthy' if (cpu_ok and memory_ok and disk_ok) else 'warning',
'message': f'CPU: {system_stats.get("cpu", {}).get("percent", 0):.1f}%, Memory: {system_stats.get("memory", {}).get("percent", 0):.1f}%, Disk: {system_stats.get("disk", {}).get("percent", 0):.1f}%'
}
if not (cpu_ok and memory_ok and disk_ok):
health_status['overall'] = 'warning'
return jsonify(health_status)
ADMIN_MAIN_VIEWS
# 5. Create User Management Views
cat > backend/app/admin/views/users.py << 'ADMIN_USER_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from app.admin import bp
from app.admin.utils import admin_required, super_admin_required, log_admin_action
from app.models.user import User
from app import db
from datetime import datetime
from werkzeug.security import generate_password_hash
@bp.route('/users')
@admin_required
def users():
"""User management page"""
log_admin_action("Accessed user management")
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
role_filter = request.args.get('role', '')
query = User.query
if search:
query = query.filter(
db.or_(
User.username.contains(search),
User.email.contains(search)
)
)
if role_filter:
query = query.filter(User.role == role_filter)
users = query.paginate(
page=page, per_page=25, error_out=False
)
return render_template('admin/users/list.html',
users=users,
search=search,
role_filter=role_filter)
@bp.route('/users/<int:user_id>')
@admin_required
def user_detail(user_id):
"""User detail page"""
user = User.query.get_or_404(user_id)
log_admin_action("Viewed user details", "user", user_id)
# Get user's certificates
certificates = user.certificates.order_by('created_at desc').limit(10).all()
# Get user's audit logs
audit_logs = user.audit_logs.order_by('timestamp desc').limit(20).all()
return render_template('admin/users/detail.html',
user=user,
certificates=certificates,
audit_logs=audit_logs)
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
@admin_required
def edit_user(user_id):
"""Edit user"""
user = User.query.get_or_404(user_id)
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
user.username = data.get('username', user.username)
user.email = data.get('email', user.email)
user.role = data.get('role', user.role)
user.is_verified = data.get('is_verified', user.is_verified) == 'true'
user.is_active = data.get('is_active', user.is_active) == 'true'
# Update password if provided
new_password = data.get('new_password')
if new_password:
user.password_hash = generate_password_hash(new_password)
try:
db.session.commit()
log_admin_action("Updated user", "user", user_id)
if request.is_json:
return jsonify({'success': True, 'message': 'User updated successfully'})
else:
flash('User updated successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user_id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error updating user: {str(e)}', 'error')
return render_template('admin/users/edit.html', user=user)
@bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
def toggle_user_status(user_id):
"""Toggle user active status"""
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
try:
db.session.commit()
action = "activated" if user.is_active else "deactivated"
log_admin_action(f"User {action}", "user", user_id)
return jsonify({
'success': True,
'message': f'User {action} successfully',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Create new user"""
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
try:
user = User(
username=data['username'],
email=data['email'],
role=data.get('role', 'user'),
is_verified=data.get('is_verified', False) == 'true',
is_active=data.get('is_active', True) == 'true'
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
log_admin_action("Created user", "user", user.id)
if request.is_json:
return jsonify({
'success': True,
'message': 'User created successfully',
'user_id': user.id
})
else:
flash('User created successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user.id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error creating user: {str(e)}', 'error')
return render_template('admin/users/create.html')
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
@super_admin_required
def delete_user(user_id):
"""Delete user (super admin only)"""
user = User.query.get_or_404(user_id)
try:
# Don't delete if user has certificates
if user.certificates.count() > 0:
return jsonify({
'success': False,
'error': 'Cannot delete user with existing certificates'
}), 400
username = user.username
db.session.delete(user)
db.session.commit()
log_admin_action("Deleted user", "user", user_id)
return jsonify({
'success': True,
'message': f'User {username} deleted successfully'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/stats')
@admin_required
def user_stats():
"""Get user statistics"""
from sqlalchemy import func
stats = {
'total_users': User.query.count(),
'active_users': User.query.filter_by(is_active=True).count(),
'verified_users': User.query.filter_by(is_verified=True).count(),
'admin_users': User.query.filter(User.role.in_(['admin', 'super_admin'])).count(),
'users_by_role': dict(
db.session.query(User.role, func.count(User.id))
.group_by(User.role)
.all()
),
'recent_signups': User.query.filter(
User.created_at >= datetime.utcnow() - timedelta(days=30)
).count()
}
return jsonify(stats)
ADMIN_USER_VIEWS
# 6. Create Certificate Management Views
cat > backend/app/admin/views/certificates.py << 'ADMIN_CERT_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from app.admin import bp
from app.admin.utils import admin_required, log_admin_action
from app.models.certificate import Certificate
from app.models.user import User
from app import db
from datetime import datetime, timedelta
from app.utils.qr_generator import generate_certificate_qr
@bp.route('/certificates')
@admin_required
def certificates():
"""Certificate management page"""
log_admin_action("Accessed certificate management")
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
search = request.args.get('search', '')
query = Certificate.query
if status_filter:
query = query.filter(Certificate.status == status_filter)
if search:
query = query.join(User).filter(
db.or_(
Certificate.product_name.contains(search),
Certificate.material_type.contains(search),
User.username.contains(search)
)
)
certificates = query.order_by(
Certificate.created_at.desc()
).paginate(page=page, per_page=25, error_out=False)
# Get statistics
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count()
}
return render_template('admin/certificates/list.html',
certificates=certificates,
stats=stats,
status_filter=status_filter,
search=search)
@bp.route('/certificates/<int:cert_id>')
@admin_required
def certificate_detail(cert_id):
"""Certificate detail page"""
certificate = Certificate.query.get_or_404(cert_id)
log_admin_action("Viewed certificate details", "certificate", cert_id)
return render_template('admin/certificates/detail.html',
certificate=certificate)
@bp.route('/certificates/<int:cert_id>/approve', methods=['POST'])
@admin_required
def approve_certificate(cert_id):
"""Approve certificate"""
certificate = Certificate.query.get_or_404(cert_id)
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'approved'
certificate.approved_at = datetime.utcnow()
certificate.approved_by = current_user.id
# Generate certificate ID and QR code
if not certificate.certificate_id:
certificate.generate_certificate_id()
# Generate QR code
qr_result = generate_certificate_qr(certificate.certificate_id)
certificate.qr_code_path = qr_result['filename']
db.session.commit()
log_admin_action("Approved certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate approved successfully',
'certificate_id': certificate.certificate_id
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/<int:cert_id>/reject', methods=['POST'])
@admin_required
def reject_certificate(cert_id):
"""Reject certificate"""
certificate = Certificate.query.get_or_404(cert_id)
data = request.get_json()
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'rejected'
certificate.rejection_reason = data.get('reason', 'No reason provided')
certificate.rejected_at = datetime.utcnow()
certificate.rejected_by = current_user.id
db.session.commit()
log_admin_action("Rejected certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate rejected successfully'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/<int:cert_id>/revoke', methods=['POST'])
@admin_required
def revoke_certificate(cert_id):
"""Revoke approved certificate"""
certificate = Certificate.query.get_or_404(cert_id)
data = request.get_json()
if certificate.status != 'approved':
return jsonify({
'success': False,
'error': 'Only approved certificates can be revoked'
}), 400
try:
certificate.status = 'revoked'
certificate.revocation_reason = data.get('reason', 'No reason provided')
certificate.revoked_at = datetime.utcnow()
certificate.revoked_by = current_user.id
db.session.commit()
log_admin_action("Revoked certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate revoked successfully'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/pending')
@admin_required
def pending_certificates():
"""Get pending certificates for approval queue"""
certificates = Certificate.query.filter_by(
status='pending'
).order_by(Certificate.created_at.asc()).all()
return jsonify([{
'id': cert.id,
'product_name': cert.product_name,
'material_type': cert.material_type,
'supplier': cert.user.username,
'created_at': cert.created_at.isoformat(),
'days_pending': (datetime.utcnow() - cert.created_at).days
} for cert in certificates])
@bp.route('/certificates/stats')
@admin_required
def certificate_stats():
"""Get certificate statistics"""
from sqlalchemy import func, extract
# Basic stats
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'revoked': Certificate.query.filter_by(status='revoked').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count()
}
# Certificates by month (last 12 months)
monthly_data = db.session.query(
extract('year', Certificate.created_at).label('year'),
extract('month', Certificate.created_at).label('month'),
func.count(Certificate.id).label('count')
).filter(
Certificate.created_at >= datetime.utcnow() - timedelta(days=365)
).group_by('year', 'month').all()
stats['monthly'] = [{
'year': int(row.year),
'month': int(row.month),
'count': row.count
} for row in monthly_data]
# Top material types
material_stats = db.session.query(
Certificate.material_type,
func.count(Certificate.id).label('count')
).group_by(Certificate.material_type).all()
stats['by_material'] = [{
'material': row[0],
'count': row[1]
} for row in material_stats]
return jsonify(stats)
CERT_VIEWS
# 7. Create Blockchain Management Views
cat > backend/app/admin/views/blockchain.py << 'ADMIN_BLOCKCHAIN_VIEWS'
from flask import render_template, request, jsonify, current_app
from app.admin import bp
from app.admin.utils import admin_required, log_admin_action, SystemMonitor
import docker
import subprocess
import os
@bp.route('/blockchain')
@admin_required
def blockchain_status():
"""Blockchain network status page"""
log_admin_action("Accessed blockchain management")
# Get Fabric network status
fabric_status = SystemMonitor.check_fabric_network()
# Get container logs (last 50 lines)
container_logs = {}
try:
client = docker.from_env()
for container_name in fabric_status.keys():
try:
container = client.containers.get(container_name)
logs = container.logs(tail=50).decode('utf-8')
container_logs[container_name] = logs.split('\n')[-50:]
except:
container_logs[container_name] = ['Container not found']
except Exception as e:
current_app.logger.error(f"Error getting container logs: {e}")
return render_template('admin/blockchain/status.html',
fabric_status=fabric_status,
container_logs=container_logs)
@bp.route('/blockchain/start-network', methods=['POST'])
@admin_required
def start_fabric_network():
"""Start Hyperledger Fabric network"""
try:
fabric_path = "/home/michal/NanoTrace/fabric-network"
script_path = os.path.join(fabric_path, "network/scripts/start_network.sh")
if not os.path.exists(script_path):
return jsonify({
'success': False,
'error': 'Fabric network script not found'
}), 400
# Run the start script
result = subprocess.run([script_path],
capture_output=True,
text=True,
timeout=300) # 5 minute timeout
log_admin_action("Started Fabric network")
if result.returncode == 0:
return jsonify({
'success': True,
'message': 'Fabric network started successfully',
'output': result.stdout
})
else:
return jsonify({
'success': False,
'error': f'Failed to start network: {result.stderr}'
}), 500
except subprocess.TimeoutExpired:
return jsonify({
'success': False,
'error': 'Network start timeout (5 minutes)'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error starting network: {str(e)}'
}), 500
@bp.route('/blockchain/stop-network', methods=['POST'])
@admin_required
def stop_fabric_network():
"""Stop Hyperledger Fabric network"""
try:
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com',
'cli'
]
stopped_containers = []
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container.stop()
stopped_containers.append(container_name)
except docker.errors.NotFound:
pass # Container already stopped
log_admin_action("Stopped Fabric network")
return jsonify({
'success': True,
'message': f'Stopped {len(stopped_containers)} containers',
'stopped': stopped_containers
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Error stopping network: {str(e)}'
}), 500
@bp.route('/blockchain/restart-network', methods=['POST'])
@admin_required
def restart_fabric_network():
"""Restart Hyperledger Fabric network"""
try:
# First stop
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com',
'cli'
]
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container.stop()
except docker.errors.NotFound:
pass
# Then start
fabric_path = "/home/michal/NanoTrace/fabric-network"
script_path = os.path.join(fabric_path, "network/scripts/start_network.sh")
result = subprocess.run([script_path],
capture_output=True,
text=True,
timeout=300)
log_admin_action("Restarted Fabric network")
if result.returncode == 0:
return jsonify({
'success': True,
'message': 'Fabric network restarted successfully'
})
else:
return jsonify({
'success': False,
'error': f'Failed to restart network: {result.stderr}'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error restarting network: {str(e)}'
}), 500
@bp.route('/blockchain/container-logs/<container_name>')
@admin_required
def get_container_logs(container_name):
"""Get logs for specific container"""
try:
client = docker.from_env()
container = client.containers.get(container_name)
lines = request.args.get('lines', 100, type=int)
logs = container.logs(tail=lines).decode('utf-8')
return jsonify({
'success': True,
'logs': logs.split('\n'),
'container': container_name
})
except docker.errors.NotFound:
return jsonify({
'success': False,
'error': f'Container {container_name} not found'
}), 404
except Exception as e:
return jsonify({
'success': False,
'error': f'Error getting logs: {str(e)}'
}), 500
@bp.route('/blockchain/chaincode-info')
@admin_required
def chaincode_info():
"""Get chaincode information"""
try:
# This would typically query the Fabric network for chaincode info
# For now, return static info based on project structure
chaincode_path = "/home/michal/NanoTrace/chaincode"
if os.path.exists(chaincode_path):
# Get Go mod info
go_mod_path = os.path.join(chaincode_path, "go.mod")
go_mod_content = ""
if os.path.exists(go_mod_path):
with open(go_mod_path, 'r') as f:
go_mod_content = f.read()
return jsonify({
'success': True,
'chaincode_path': chaincode_path,
'go_mod': go_mod_content,
'status': 'Available (not deployed)'
})
else:
return jsonify({
'success': False,
'error': 'Chaincode directory not found'
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Error getting chaincode info: {str(e)}'
}), 500
ADMIN_BLOCKCHAIN_VIEWS
# 8. Create System Management Views
cat > backend/app/admin/views/system.py << 'ADMIN_SYSTEM_VIEWS'
from flask import render_template, request, jsonify, current_app, send_file
from app.admin import bp
from app.admin.utils import admin_required, super_admin_required, log_admin_action, SystemMonitor, format_bytes
from app.models.admin_models import SystemConfig, SystemMetric
from app import db
from datetime import datetime, timedelta
import os
import subprocess
import tempfile
import json
import shutil
@bp.route('/system')
@admin_required
def system_overview():
"""System overview page"""
log_admin_action("Accessed system management")
system_stats = SystemMonitor.get_system_stats()
# Get system configuration
configs = SystemConfig.query.all()
config_dict = {config.key: config.value for config in configs}
# Get disk usage for important directories
directories = [
'/home/michal/NanoTrace',
'/var/log',
'/tmp',
'/home/michal/NanoTrace/backend/app/static/qr_codes'
]
disk_usage = {}
for directory in directories:
if os.path.exists(directory):
try:
usage = shutil.disk_usage(directory)
disk_usage[directory] = {
'total': usage.total,
'used': usage.used,
'free': usage.free,
'total_formatted': format_bytes(usage.total),
'used_formatted': format_bytes(usage.used),
'free_formatted': format_bytes(usage.free)
}
except:
disk_usage[directory] = None
return render_template('admin/system/overview.html',
system_stats=system_stats,
config_dict=config_dict,
disk_usage=disk_usage)
@bp.route('/system/config')
@admin_required
def system_config():
"""System configuration management"""
configs = SystemConfig.query.order_by(SystemConfig.key).all()
return render_template('admin/system/config.html', configs=configs)
@bp.route('/system/config/update', methods=['POST'])
@admin_required
def update_system_config():
"""Update system configuration"""
data = request.get_json()
key = data.get('key')
value = data.get('value')
description = data.get('description')
if not key:
return jsonify({'success': False, 'error': 'Key is required'}), 400
try:
config = SystemConfig.set_config(key, value, current_user.id, description)
log_admin_action(f"Updated config: {key}", "system_config", key)
return jsonify({
'success': True,
'message': f'Configuration {key} updated successfully'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/system/logs')
@admin_required
def system_logs():
"""View system logs"""
log_type = request.args.get('type', 'flask')
lines = request.args.get('lines', 100, type=int)
log_files = {
'flask': '/var/log/nanotrace/app.log',
'nginx': '/var/log/nginx/access.log',
'nginx_error': '/var/log/nginx/error.log',
'system': '/var/log/syslog',
'postgresql': '/var/log/postgresql/postgresql-16-main.log'
}
log_content = []
error_message = None
if log_type in log_files:
log_file = log_files[log_type]
try:
if os.path.exists(log_file):
# Use tail command to get last N lines
result = subprocess.run(['tail', '-n', str(lines), log_file],
capture_output=True, text=True)
if result.returncode == 0:
log_content = result.stdout.strip().split('\n')
else:
error_message = f"Error reading log: {result.stderr}"
else:
error_message = f"Log file not found: {log_file}"
except Exception as e:
error_message = f"Error accessing log: {str(e)}"
else:
error_message = "Invalid log type"
return render_template('admin/system/logs.html',
log_content=log_content,
log_type=log_type,
error_message=error_message,
available_logs=list(log_files.keys()))
@bp.route('/system/backup', methods=['POST'])
@super_admin_required
def create_backup():
"""Create system backup"""
try:
backup_dir = "/tmp/nanotrace_backup_" + datetime.now().strftime("%Y%m%d_%H%M%S")
os.makedirs(backup_dir, exist_ok=True)
# Database backup
db_backup_file = os.path.join(backup_dir, "database.sql")
db_cmd = ["pg_dump", "-h", "localhost", "-U", "nanotrace", "-d", "nanotrace", "-f", db_backup_file]
subprocess.run(db_cmd, check=True, env={"PGPASSWORD": "password"})
# Application files backup (excluding venv and logs)
app_backup_file = os.path.join(backup_dir, "application.tar.gz")
tar_cmd = ["tar", "-czf", app_backup_file,
"--exclude=venv", "--exclude=*.log", "--exclude=__pycache__",
"-C", "/home/michal", "NanoTrace"]
subprocess.run(tar_cmd, check=True)
# Create backup archive
final_backup = f"/tmp/nanotrace_full_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.tar.gz"
final_cmd = ["tar", "-czf", final_backup, "-C", "/tmp", os.path.basename(backup_dir)]
subprocess.run(final_cmd, check=True)
# Cleanup temp directory
shutil.rmtree(backup_dir)
log_admin_action("Created system backup", "system", final_backup)
return jsonify({
'success': True,
'message': 'Backup created successfully',
'backup_file': final_backup,
'size': format_bytes(os.path.getsize(final_backup))
})
except subprocess.CalledProcessError as e:
return jsonify({
'success': False,
'error': f'Backup command failed: {str(e)}'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Backup failed: {str(e)}'
}), 500
@bp.route('/system/cleanup', methods=['POST'])
@admin_required
def system_cleanup():
"""Clean up temporary files and old data"""
cleaned_items = []
total_freed = 0
try:
# Clean old QR codes (older than 30 days)
from app.utils.qr_generator import QRCodeGenerator
generator = QRCodeGenerator()
qr_cleaned = generator.cleanup_old_qr_codes(30)
if qr_cleaned:
cleaned_items.append(f"Removed {qr_cleaned} old QR codes")
# Clean old metrics (older than 90 days)
cutoff_date = datetime.utcnow() - timedelta(days=90)
old_metrics = SystemMetric.query.filter(SystemMetric.timestamp < cutoff_date)
metrics_count = old_metrics.count()
if metrics_count > 0:
old_metrics.delete()
db.session.commit()
cleaned_items.append(f"Removed {metrics_count} old metrics")
# Clean temp files
temp_dirs = ['/tmp', '/var/tmp']
for temp_dir in temp_dirs:
if os.path.exists(temp_dir):
for item in os.listdir(temp_dir):
if item.startswith('nanotrace_'):
item_path = os.path.join(temp_dir, item)
if os.path.isfile(item_path):
size = os.path.getsize(item_path)
os.remove(item_path)
total_freed += size
elif os.path.isdir(item_path):
size = sum(os.path.getsize(os.path.join(dirpath, filename))
for dirpath, dirnames, filenames in os.walk(item_path)
for filename in filenames)
shutil.rmtree(item_path)
total_freed += size
if total_freed > 0:
cleaned_items.append(f"Freed {format_bytes(total_freed)} from temp files")
log_admin_action("Performed system cleanup")
return jsonify({
'success': True,
'message': 'System cleanup completed',
'cleaned_items': cleaned_items,
'total_freed': format_bytes(total_freed)
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Cleanup failed: {str(e)}'
}), 500
@bp.route('/system/services')
@admin_required
def system_services():
"""View system services status"""
services = [
'postgresql',
'redis-server',
'nginx',
'nanotrace' # Our Flask app service
]
service_status = {}
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
# Get additional info
info_result = subprocess.run(['systemctl', 'show', service,
'--property=LoadState,ActiveState,SubState,MainPID'],
capture_output=True, text=True)
service_info = {}
for line in info_result.stdout.strip().split('\n'):
if '=' in line:
key, value = line.split('=', 1)
service_info[key] = value
service_status[service] = {
'status': status,
'active': status == 'active',
'info': service_info
}
except Exception as e:
service_status[service] = {
'status': 'error',
'active': False,
'error': str(e)
}
return render_template('admin/system/services.html',
services=service_status)
@bp.route('/system/services/<service_name>/<action>', methods=['POST'])
@super_admin_required
def control_service(service_name, action):
"""Control system services"""
allowed_services = ['postgresql', 'redis-server', 'nginx', 'nanotrace']
allowed_actions = ['start', 'stop', 'restart', 'reload']
if service_name not in allowed_services or action not in allowed_actions:
return jsonify({
'success': False,
'error': 'Invalid service or action'
}), 400
try:
result = subprocess.run(['sudo', 'systemctl', action, service_name],
capture_output=True, text=True, timeout=30)
log_admin_action(f"{action.title()} service: {service_name}", "service", service_name)
if result.returncode == 0:
return jsonify({
'success': True,
'message': f'Service {service_name} {action} completed'
})
else:
return jsonify({
'success': False,
'error': f'Service {action} failed: {result.stderr}'
}), 500
except subprocess.TimeoutExpired:
return jsonify({
'success': False,
'error': 'Service operation timed out'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error controlling service: {str(e)}'
}), 500
@bp.route('/system/export-data')
@super_admin_required
def export_system_data():
"""Export system data for analysis"""
try:
# Create temporary file for export
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
export_data = {
'export_date': datetime.utcnow().isoformat(),
'system_stats': SystemMonitor.get_system_stats(),
'database_stats': SystemMonitor.get_database_stats(),
'fabric_status': SystemMonitor.check_fabric_network(),
'recent_metrics': [metric.to_dict() for metric in
SystemMetric.query.order_by(
SystemMetric.timestamp.desc()
).limit(1000).all()],
'recent_audit_logs': [log.to_dict() for log in
AuditLog.query.order_by(
AuditLog.timestamp.desc()
).limit(500).all()]
}
json.dump(export_data, temp_file, indent=2)
temp_file.close()
log_admin_action("Exported system data")
return send_file(
temp_file.name,
as_attachment=True,
download_name=f"nanotrace_system_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
mimetype='application/json'
)
except Exception as e:
return jsonify({
'success': False,
'error': f'Export failed: {str(e)}'
}), 500
ADMIN_SYSTEM_VIEWSChatGPT said:
Nice
drop. I see a few gotchas that will bite you when you run this as-is.
Here’s a tight set of fixes so the admin console boots cleanly under
your backend.* package and the templates behave.
Do these fixes
1) Fix import paths (app. → backend.app.)
Your files use from app ... but your package is backend.app. One-liner to repair everything you just wrote:
bashcd ~/NanoTrace
grep -RIl "from app\." backend | xargs sed -i 's/from app\./from backend.app\./g'
grep -RIl "import app" backend | xargs sed -i 's/import app/import backend.app as app/g'
And in the admin blueprint init file:
bash# backend/app/admin/__init__.py
# change:
# from app.admin.views import main, users, certificates, blockchain, system
# to:
from backend.app.admin.views import main, users, certificates, blockchain, system
2) Register the blueprint with the correct import
Open backend/app/__init__.py and, inside create_app() after your other blueprints:
pythonfrom backend.app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix="/admin")
(If your file currently appends a block that says from app.admin ..., remove it or replace it with the above.)
3) Fix the boolean in the Users template
Jinja’s |lower on a boolean can produce strings, which breaks the JS confirm. Use |tojson so it’s a real JS boolean:
html<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | tojson }})"
(That’s in backend/app/admin/templates/users/list.html.)
4) Make sure model fields exist
Your templates and views expect these columns. Add them if missing and migrate.
User (backend/app/models/user.py):
pythonfrom datetime import datetime
from backend.app import db
role = db.Column(db.String(20), default="user", nullable=False)
is_verified = db.Column(db.Boolean, default=False, nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
last_login = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
Certificate (backend/app/models/certificate.py):
pythonfrom datetime import datetime
from backend.app import db
# approval workflow
status = db.Column(db.String(20), default="pending", nullable=False)
approved_at = db.Column(db.DateTime)
approved_by = db.Column(db.Integer, db.ForeignKey("users.id"))
rejected_at = db.Column(db.DateTime)
rejected_by = db.Column(db.Integer, db.ForeignKey("users.id"))
rejection_reason = db.Column(db.Text)
revoked_at = db.Column(db.DateTime)
revoked_by = db.Column(db.Integer, db.ForeignKey("users.id"))
revocation_reason = db.Column(db.Text)
qr_code_path = db.Column(db.String(255))
Then migrate:
bashcd ~/NanoTrace
source venv/bin/activate
export FLASK_APP=backend.app:create_app
flask db migrate -m "admin fields"
flask db upgrade
5) Ensure the views import the right things
Quick spot checks:
-
backend/app/admin/views/main.py-
from backend.app.admin import bp -
from backend.app.admin.utils import admin_required, SystemMonitor, log_admin_action -
from backend.app.models.admin_models import SystemMetric, AuditLog
-
-
backend/app/admin/views/users.py-
from backend.app.admin import bp -
from backend.app.admin.utils import admin_required, super_admin_required, log_admin_action -
from backend.app.models.user import User -
from backend.app import db
-
-
backend/app/admin/views/certificates.py-
Change all
from app...tofrom backend.app... -
If you reference
current_user, addfrom flask_login import current_user.
-
-
backend/app/admin/views/blockchain.pyandsystem.py-
Same
backend.appimport fixes. -
Add
from flask_login import current_userwhere you use it.
-
6) Base template + static
You already saved admin_base.html at backend/app/admin/templates/admin_base.html. Good. Your CSS is at backend/app/admin/static/css/admin.css. In the base template’s <head> include it:
html<link rel="stylesheet" href="{{ url_for('admin.static', filename='css/admin.css') }}">
7) Add minimal metrics API (optional quick win)
The dashboard JS calls /admin/api/metrics. You already included a handler in main.py, but it reads from SystemMetric.
If you haven’t started recording metrics yet, keep it but expect an
empty chart. To start collecting, wire a cron or systemd timer that
calls SystemMonitor.record_metrics() (your scripts/monitor_services.py does that—just fix its imports as in step 1).
Also, if you use that cron, create the log file directory:
bashsudo touch /var/log/nanotrace_monitor.log
sudo chown michal:www-data /var/log/nanotrace_monitor.log
8) Security nits to address soon
-
In
system.pybackup, you’ve hardcodedPGPASSWORD="password". Move DB creds to env vars and read withos.getenv. -
The system/service control endpoints call
sudo systemctl. Make sure your sudoers are locked down if you keep that.
9) Restart the app
bashsudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
Then hit: http(s)://<your-domain>/admin/dashboard.
If anything 500s, send me the traceback (from journalctl -u nanotrace -n 200 --no-pager)
and I’ll zero in, but the changes above fix the biggest blockers:
package paths, boolean in JS, blueprint registration, and migrations.
You said:
#!/bin/bash
# =============================================================================
# NanoTrace Admin Dashboard Builder - Complete Fixed Version
# Creates a sophisticated Flask admin dashboard for system management
# =============================================================================
set -e
echo "Building NanoTrace Admin Dashboard..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# Create admin blueprint structure
mkdir -p backend/app/admin/{views,templates,static}
mkdir -p backend/app/admin/static/{css,js}
mkdir -p backend/app/admin/templates/{base,dashboard,users,certificates,blockchain,system}
# 1. Create Admin Blueprint
cat > backend/app/admin/__init__.py << 'ADMIN_INIT'
from flask import Blueprint
bp = Blueprint('admin', __name__,
template_folder='templates',
static_folder='static',
static_url_path='/admin/static')
from backend.app.admin.views import main, users, certificates, blockchain, system
ADMIN_INIT
# 2. Create Admin Models
cat > backend/app/models/admin_models.py << 'ADMIN_MODELS'
from datetime import datetime
from backend.app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import enum
class UserRole(enum.Enum):
USER = "user"
ADMIN = "admin"
SUPER_ADMIN = "super_admin"
class SystemMetric(db.Model):
"""Track system metrics and health"""
__tablename__ = 'system_metrics'
id = db.Column(db.Integer, primary_key=True)
metric_name = db.Column(db.String(100), nullable=False)
metric_value = db.Column(db.Float, nullable=False)
metric_unit = db.Column(db.String(20))
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.metric_name,
'value': self.metric_value,
'unit': self.metric_unit,
'timestamp': self.timestamp.isoformat()
}
class AuditLog(db.Model):
"""Track admin actions for security"""
__tablename__ = 'audit_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
action = db.Column(db.String(200), nullable=False)
resource = db.Column(db.String(100))
resource_id = db.Column(db.String(100))
ip_address = db.Column(db.String(45))
user_agent = db.Column(db.Text)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref=db.backref('audit_logs', lazy=True))
def to_dict(self):
return {
'id': self.id,
'user': self.user.username if self.user else 'Unknown',
'action': self.action,
'resource': self.resource,
'resource_id': self.resource_id,
'ip': self.ip_address,
'timestamp': self.timestamp.isoformat()
}
class SystemConfig(db.Model):
"""Store system configuration settings"""
__tablename__ = 'system_config'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.Text)
description = db.Column(db.Text)
updated_by = db.Column(db.Integer, db.ForeignKey('users.id'))
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
updater = db.relationship('User')
@staticmethod
def get_config(key, default=None):
config = SystemConfig.query.filter_by(key=key).first()
return config.value if config else default
@staticmethod
def set_config(key, value, user_id, description=None):
config = SystemConfig.query.filter_by(key=key).first()
if config:
config.value = value
config.updated_by = user_id
config.updated_at = datetime.utcnow()
else:
config = SystemConfig(
key=key,
value=value,
description=description,
updated_by=user_id
)
db.session.add(config)
db.session.commit()
return config
ADMIN_MODELS
# 3. Create Admin Utils
cat > backend/app/admin/utils.py << 'ADMIN_UTILS'
from functools import wraps
from flask import abort, request, current_app
from flask_login import current_user
import psutil
import time
import requests
from datetime import datetime, timedelta
from backend.app.models.admin_models import SystemMetric, AuditLog
from backend.app import db
def admin_required(f):
"""Decorator to require admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role not in ['admin', 'super_admin']:
abort(403)
return f(*args, **kwargs)
return decorated_function
def super_admin_required(f):
"""Decorator to require super admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != 'super_admin':
abort(403)
return f(*args, **kwargs)
return decorated_function
def log_admin_action(action, resource=None, resource_id=None):
"""Log admin actions for audit trail"""
try:
log_entry = AuditLog(
user_id=current_user.id,
action=action,
resource=resource,
resource_id=str(resource_id) if resource_id else None,
ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent', '')[:500]
)
db.session.add(log_entry)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Failed to log admin action: {e}")
class SystemMonitor:
"""System monitoring utilities"""
@staticmethod
def get_system_stats():
"""Get current system statistics"""
try:
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
# Network stats (simplified)
network = psutil.net_io_counters()
return {
'cpu': {
'percent': cpu_percent,
'count': psutil.cpu_count()
},
'memory': {
'total': memory.total,
'available': memory.available,
'percent': memory.percent,
'used': memory.used
},
'disk': {
'total': disk.total,
'used': disk.used,
'free': disk.free,
'percent': (disk.used / disk.total) * 100
},
'network': {
'bytes_sent': network.bytes_sent,
'bytes_recv': network.bytes_recv
},
'timestamp': datetime.utcnow().isoformat()
}
except Exception as e:
current_app.logger.error(f"Error getting system stats: {e}")
return {}
@staticmethod
def record_metrics():
"""Record system metrics to database"""
stats = SystemMonitor.get_system_stats()
if not stats:
return
try:
metrics = [
SystemMetric(metric_name='cpu_percent', metric_value=stats['cpu']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_percent', metric_value=stats['memory']['percent'], metric_unit='%'),
SystemMetric(metric_name='disk_percent', metric_value=stats['disk']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_used', metric_value=stats['memory']['used'], metric_unit='bytes'),
SystemMetric(metric_name='disk_used', metric_value=stats['disk']['used'], metric_unit='bytes'),
]
for metric in metrics:
db.session.add(metric)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Error recording metrics: {e}")
db.session.rollback()
@staticmethod
def check_fabric_network():
"""Check Hyperledger Fabric network status"""
try:
# Check if Fabric containers are running
import docker
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com'
]
container_status = {}
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container_status[container_name] = {
'status': container.status,
'healthy': container.status == 'running'
}
except:
container_status[container_name] = {
'status': 'not_found',
'healthy': False
}
return container_status
except Exception as e:
current_app.logger.error(f"Error checking Fabric network: {e}")
return {}
@staticmethod
def get_database_stats():
"""Get database statistics"""
try:
from backend.app.models.user import User
from backend.app.models.certificate import Certificate
stats = {
'total_users': User.query.count(),
'admin_users': User.query.filter_by(role='admin').count(),
'total_certificates': Certificate.query.count(),
'pending_certificates': Certificate.query.filter_by(status='pending').count(),
'approved_certificates': Certificate.query.filter_by(status='approved').count(),
'rejected_certificates': Certificate.query.filter_by(status='rejected').count(),
}
return stats
except Exception as e:
current_app.logger.error(f"Error getting database stats: {e}")
return {}
def format_bytes(bytes):
"""Format bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes < 1024.0:
return f"{bytes:.1f} {unit}"
bytes /= 1024.0
return f"{bytes:.1f} PB"
def format_uptime(seconds):
"""Format uptime in human readable format"""
days = seconds // 86400
hours = (seconds % 86400) // 3600
minutes = (seconds % 3600) // 60
parts = []
if days:
parts.append(f"{int(days)}d")
if hours:
parts.append(f"{int(hours)}h")
if minutes:
parts.append(f"{int(minutes)}m")
return " ".join(parts) if parts else "0m"
ADMIN_UTILS
# 4. Create Views Directory Structure
mkdir -p backend/app/admin/views
# 5. Create Main Admin Views
cat > backend/app/admin/views/main.py << 'ADMIN_MAIN_VIEWS'
from flask import render_template, jsonify, request, current_app
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, SystemMonitor, log_admin_action
from backend.app.models.admin_models import SystemMetric, AuditLog
from datetime import datetime, timedelta
from sqlalchemy import func
@bp.route('/dashboard')
@admin_required
def dashboard():
"""Main admin dashboard"""
log_admin_action("Accessed admin dashboard")
# Get system stats
system_stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
fabric_status = SystemMonitor.check_fabric_network()
# Get recent metrics for charts (last 24 hours)
since = datetime.utcnow() - timedelta(hours=24)
recent_metrics = SystemMetric.query.filter(
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.desc()).limit(100).all()
# Get recent audit logs
recent_logs = AuditLog.query.order_by(
AuditLog.timestamp.desc()
).limit(20).all()
return render_template('dashboard/main.html',
system_stats=system_stats,
db_stats=db_stats,
fabric_status=fabric_status,
recent_metrics=recent_metrics,
recent_logs=recent_logs)
@bp.route('/api/system-stats')
@admin_required
def api_system_stats():
"""API endpoint for real-time system stats"""
stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
return jsonify({
'system': stats,
'database': db_stats,
'timestamp': datetime.utcnow().isoformat()
})
@bp.route('/api/metrics')
@admin_required
def api_metrics():
"""API endpoint for metrics data"""
hours = request.args.get('hours', 24, type=int)
metric_name = request.args.get('metric', 'cpu_percent')
since = datetime.utcnow() - timedelta(hours=hours)
metrics = SystemMetric.query.filter(
SystemMetric.metric_name == metric_name,
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.asc()).all()
data = [{
'timestamp': metric.timestamp.isoformat(),
'value': metric.metric_value
} for metric in metrics]
return jsonify(data)
@bp.route('/health-check')
@admin_required
def health_check():
"""Comprehensive system health check"""
log_admin_action("Performed health check")
health_status = {
'timestamp': datetime.utcnow().isoformat(),
'overall': 'healthy',
'checks': {}
}
# Database check
try:
from backend.app import db
db.session.execute('SELECT 1').fetchone()
health_status['checks']['database'] = {
'status': 'healthy',
'message': 'Database connection successful'
}
except Exception as e:
health_status['checks']['database'] = {
'status': 'unhealthy',
'message': f'Database error: {str(e)}'
}
health_status['overall'] = 'degraded'
# Fabric network check
fabric_status = SystemMonitor.check_fabric_network()
all_healthy = all(container['healthy'] for container in fabric_status.values())
health_status['checks']['fabric'] = {
'status': 'healthy' if all_healthy else 'unhealthy',
'message': f'Fabric containers: {len([c for c in fabric_status.values() if c["healthy"]])}/{len(fabric_status)} healthy',
'details': fabric_status
}
if not all_healthy:
health_status['overall'] = 'degraded'
return jsonify(health_status)
ADMIN_MAIN_VIEWS
# 6. Create User Management Views
cat > backend/app/admin/views/users.py << 'ADMIN_USER_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, super_admin_required, log_admin_action
from backend.app.models.user import User
from backend.app import db
from datetime import datetime
from werkzeug.security import generate_password_hash
@bp.route('/users')
@admin_required
def users():
"""User management page"""
log_admin_action("Accessed user management")
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
role_filter = request.args.get('role', '')
query = User.query
if search:
query = query.filter(
db.or_(
User.username.contains(search),
User.email.contains(search)
)
)
if role_filter:
query = query.filter(User.role == role_filter)
users = query.paginate(
page=page, per_page=25, error_out=False
)
return render_template('users/list.html',
users=users,
search=search,
role_filter=role_filter)
@bp.route('/users/<int:user_id>')
@admin_required
def user_detail(user_id):
"""User detail page"""
user = User.query.get_or_404(user_id)
log_admin_action("Viewed user details", "user", user_id)
# Get user's certificates
certificates = user.certificates.order_by('created_at desc').limit(10).all()
# Get user's audit logs
audit_logs = user.audit_logs.order_by('timestamp desc').limit(20).all()
return render_template('users/detail.html',
user=user,
certificates=certificates,
audit_logs=audit_logs)
@bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
def toggle_user_status(user_id):
"""Toggle user active status"""
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
try:
db.session.commit()
action = "activated" if user.is_active else "deactivated"
log_admin_action(f"User {action}", "user", user_id)
return jsonify({
'success': True,
'message': f'User {action} successfully',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Create new user"""
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
try:
user = User(
username=data['username'],
email=data['email'],
role=data.get('role', 'user'),
is_verified=data.get('is_verified', False) == 'true',
is_active=data.get('is_active', True) == 'true'
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
log_admin_action("Created user", "user", user.id)
if request.is_json:
return jsonify({
'success': True,
'message': 'User created successfully',
'user_id': user.id
})
else:
flash('User created successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user.id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error creating user: {str(e)}', 'error')
return render_template('users/create.html')
ADMIN_USER_VIEWS
# 7. Create Certificate Management Views
cat > backend/app/admin/views/certificates.py << 'ADMIN_CERT_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, log_admin_action
from backend.app.models.certificate import Certificate
from backend.app.models.user import User
from backend.app import db
from datetime import datetime, timedelta
@bp.route('/certificates')
@admin_required
def certificates():
"""Certificate management page"""
log_admin_action("Accessed certificate management")
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
search = request.args.get('search', '')
query = Certificate.query
if status_filter:
query = query.filter(Certificate.status == status_filter)
if search:
query = query.join(User).filter(
db.or_(
Certificate.product_name.contains(search),
Certificate.material_type.contains(search),
User.username.contains(search)
)
)
certificates = query.order_by(
Certificate.created_at.desc()
).paginate(page=page, per_page=25, error_out=False)
# Get statistics
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count()
}
return render_template('certificates/list.html',
certificates=certificates,
stats=stats,
status_filter=status_filter,
search=search)
@bp.route('/certificates/<int:cert_id>/approve', methods=['POST'])
@admin_required
def approve_certificate(cert_id):
"""Approve certificate"""
certificate = Certificate.query.get_or_404(cert_id)
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'approved'
certificate.approved_at = datetime.utcnow()
certificate.approved_by = current_user.id
# Generate certificate ID
if not certificate.certificate_id:
certificate.generate_certificate_id()
db.session.commit()
log_admin_action("Approved certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate approved successfully',
'certificate_id': certificate.certificate_id
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/pending')
@admin_required
def pending_certificates():
"""Get pending certificates for approval queue"""
certificates = Certificate.query.filter_by(
status='pending'
).order_by(Certificate.created_at.asc()).all()
return jsonify([{
'id': cert.id,
'product_name': cert.product_name,
'material_type': cert.material_type,
'supplier': cert.user.username,
'created_at': cert.created_at.isoformat(),
'days_pending': (datetime.utcnow() - cert.created_at).days
} for cert in certificates])
ADMIN_CERT_VIEWS
# 8. Create Blockchain Management Views
cat > backend/app/admin/views/blockchain.py << 'ADMIN_BLOCKCHAIN_VIEWS'
from flask import render_template, request, jsonify, current_app
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, log_admin_action, SystemMonitor
import docker
import subprocess
import os
@bp.route('/blockchain')
@admin_required
def blockchain_status():
"""Blockchain network status page"""
log_admin_action("Accessed blockchain management")
# Get Fabric network status
fabric_status = SystemMonitor.check_fabric_network()
return render_template('blockchain/status.html',
fabric_status=fabric_status)
@bp.route('/blockchain/start-network', methods=['POST'])
@admin_required
def start_fabric_network():
"""Start Hyperledger Fabric network"""
try:
fabric_path = "/home/michal/NanoTrace/fabric-network"
script_path = os.path.join(fabric_path, "network/scripts/start_network.sh")
if not os.path.exists(script_path):
return jsonify({
'success': False,
'error': 'Fabric network script not found'
}), 400
# Run the start script
result = subprocess.run([script_path],
capture_output=True,
text=True,
timeout=300) # 5 minute timeout
log_admin_action("Started Fabric network")
if result.returncode == 0:
return jsonify({
'success': True,
'message': 'Fabric network started successfully',
'output': result.stdout
})
else:
return jsonify({
'success': False,
'error': f'Failed to start network: {result.stderr}'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error starting network: {str(e)}'
}), 500
@bp.route('/blockchain/stop-network', methods=['POST'])
@admin_required
def stop_fabric_network():
"""Stop Hyperledger Fabric network"""
try:
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com',
'cli'
]
stopped_containers = []
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container.stop()
stopped_containers.append(container_name)
except docker.errors.NotFound:
pass # Container already stopped
log_admin_action("Stopped Fabric network")
return jsonify({
'success': True,
'message': f'Stopped {len(stopped_containers)} containers',
'stopped': stopped_containers
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Error stopping network: {str(e)}'
}), 500
ADMIN_BLOCKCHAIN_VIEWS
# 9. Create System Management Views
cat > backend/app/admin/views/system.py << 'ADMIN_SYSTEM_VIEWS'
from flask import render_template, request, jsonify, current_app
from flask_login import current_user
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, super_admin_required, log_admin_action, SystemMonitor, format_bytes
from backend.app.models.admin_models import SystemConfig, SystemMetric
from backend.app import db
from datetime import datetime, timedelta
import os
import subprocess
import shutil
@bp.route('/system')
@admin_required
def system_overview():
"""System overview page"""
log_admin_action("Accessed system management")
system_stats = SystemMonitor.get_system_stats()
# Get system configuration
configs = SystemConfig.query.all()
config_dict = {config.key: config.value for config in configs}
# Get disk usage for important directories
directories = [
'/home/michal/NanoTrace',
'/var/log',
'/tmp'
]
disk_usage = {}
for directory in directories:
if os.path.exists(directory):
try:
usage = shutil.disk_usage(directory)
disk_usage[directory] = {
'total': usage.total,
'used': usage.used,
'free': usage.free,
'total_formatted': format_bytes(usage.total),
'used_formatted': format_bytes(usage.used),
'free_formatted': format_bytes(usage.free)
}
except:
disk_usage[directory] = None
return render_template('system/overview.html',
system_stats=system_stats,
config_dict=config_dict,
disk_usage=disk_usage)
@bp.route('/system/services')
@admin_required
def system_services():
"""View system services status"""
services = [
'postgresql',
'redis-server',
'nginx',
'nanotrace' # Our Flask app service
]
service_status = {}
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
service_status[service] = {
'status': status,
'active': status == 'active'
}
except Exception as e:
service_status[service] = {
'status': 'error',
'active': False,
'error': str(e)
}
return render_template('system/services.html',
services=service_status)
ADMIN_SYSTEM_VIEWS
# 10. Create Admin Templates
# Base template
cat > backend/app/admin/templates/admin_base.html << 'ADMIN_BASE_TEMPLATE'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}NanoTrace Admin Dashboard{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.sidebar-transition {
transition: all 0.3s ease-in-out;
}
.admin-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-healthy { background-color: #10b981; }
.status-warning { background-color: #f59e0b; }
.status-error { background-color: #ef4444; }
.metric-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
</style>
</head>
<body class="bg-gray-100">
<div class="flex min-h-screen">
<!-- Sidebar -->
<aside class="bg-gray-900 text-white w-64 min-h-screen sidebar-transition" id="sidebar">
<div class="p-4">
<div class="flex items-center mb-8">
<i class="fas fa-cog text-2xl mr-3 text-blue-400"></i>
<h1 class="text-xl font-bold">Admin Panel</h1>
</div>
<nav class="space-y-2">
<a href="{{ url_for('admin.dashboard') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-tachometer-alt mr-3"></i>
Dashboard
</a>
<a href="{{ url_for('admin.users') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-users mr-3"></i>
Users
</a>
<a href="{{ url_for('admin.certificates') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-certificate mr-3"></i>
Certificates
</a>
<a href="{{ url_for('admin.blockchain_status') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-link mr-3"></i>
Blockchain
</a>
<a href="{{ url_for('admin.system_overview') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-server mr-3"></i>
System
</a>
<hr class="my-4 border-gray-700">
<a href="{{ url_for('main.index') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-home mr-3"></i>
Back to Site
</a>
<a href="{{ url_for('auth.logout') }}"
class="flex items-center px-4 py-2 text-red-400 hover:bg-red-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-sign-out-alt mr-3"></i>
Logout
</a>
</nav>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-hidden">
<!-- Top Header -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex items-center">
<button id="sidebar-toggle" class="text-gray-500 hover:text-gray-700 mr-4">
<i class="fas fa-bars"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">{% block page_title %}Dashboard{% endblock %}</h2>
</div>
<div class="flex items-center space-x-4">
<!-- System Status Indicator -->
<div class="flex items-center">
<span class="status-indicator status-healthy" id="system-status"></span>
<span class="text-sm text-gray-600">System Status</span>
</div>
<!-- User Info -->
<div class="flex items-center">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium mr-2">
{{ current_user.username[0].upper() }}
</div>
<span class="text-sm text-gray-700">{{ current_user.username }}</span>
</div>
</div>
</div>
</header>
<!-- Page Content -->
<div class="p-6 overflow-y-auto" style="height: calc(100vh - 73px);">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mb-4">
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} bg-{{ 'green' if category == 'success' else 'red' }}-100 border border-{{ 'green' if category == 'success' else 'red' }}-400 text-{{ 'green' if category == 'success' else 'red' }}-700 px-4 py-3 rounded mb-2">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 flex items-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<span>Loading...</span>
</div>
</div>
<!-- Scripts -->
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', function() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-translate-x-full');
});
// System status check
function checkSystemStatus() {
fetch('/admin/api/system-stats')
.then(response => response.json())
.then(data => {
const indicator = document.getElementById('system-status');
// Simple health check based on CPU and memory
const cpu = data.system.cpu?.percent || 0;
const memory = data.system.memory?.percent || 0;
if (cpu > 90 || memory > 90) {
indicator.className = 'status-indicator status-error';
} else if (cpu > 70 || memory > 70) {
indicator.className = 'status-indicator status-warning';
} else {
indicator.className = 'status-indicator status-healthy';
}
})
.catch(() => {
document.getElementById('system-status').className = 'status-indicator status-error';
});
}
// Check status every 30 seconds
checkSystemStatus();
setInterval(checkSystemStatus, 30000);
// Loading overlay functions
function showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>
ADMIN_BASE_TEMPLATE
# Main dashboard template
cat > backend/app/admin/templates/dashboard/main.html << 'DASHBOARD_MAIN_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<!-- Quick Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- System Health -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-heartbeat text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">System Health</p>
<p class="text-2xl font-bold text-gray-900">
{% if system_stats.cpu.percent < 80 %}Healthy{% else %}Warning{% endif %}
</p>
</div>
</div>
</div>
<!-- Total Users -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<i class="fas fa-users text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Users</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_users or 0 }}</p>
</div>
</div>
</div>
<!-- Certificates -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
<i class="fas fa-certificate text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Certificates</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_certificates or 0 }}</p>
<p class="text-xs text-gray-500">{{ db_stats.pending_certificates or 0 }} pending</p>
</div>
</div>
</div>
<!-- Blockchain Status -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<i class="fas fa-link text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Blockchain</p>
{% set healthy_containers = fabric_status.values() | selectattr('healthy') | list | length %}
<p class="text-2xl font-bold text-gray-900">
{{ healthy_containers }}/{{ fabric_status | length }}
</p>
<p class="text-xs text-gray-500">containers running</p>
</div>
</div>
</div>
</div>
<!-- System Overview -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- System Resources -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">System Resources</h3>
<button id="refresh-stats" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="space-y-4">
<!-- CPU Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>CPU Usage</span>
<span>{{ "%.1f" | format(system_stats.cpu.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ system_stats.cpu.percent }}%"></div>
</div>
</div>
<!-- Memory Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Memory Usage</span>
<span>{{ "%.1f" | format(system_stats.memory.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full" style="width: {{ system_stats.memory.percent }}%"></div>
</div>
</div>
<!-- Disk Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Disk Usage</span>
<span>{{ "%.1f" | format(system_stats.disk.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-orange-600 h-2 rounded-full" style="width: {{ system_stats.disk.percent }}%"></div>
</div>
</div>
</div>
</div>
<!-- Fabric Network Status -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Blockchain Network</h3>
<div class="flex space-x-2">
<button id="start-network" class="px-3 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600">
Start
</button>
<button id="stop-network" class="px-3 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600">
Stop
</button>
</div>
</div>
<div class="space-y-3">
{% for container_name, status in fabric_status.items() %}
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if status.healthy else 'error' }}"></span>
<span class="text-sm font-medium text-gray-700">{{ container_name }}</span>
</div>
<span class="text-xs px-2 py-1 rounded-full {{ 'bg-green-100 text-green-800' if status.healthy else 'bg-red-100 text-red-800' }}">
{{ status.status }}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Pending Certificates -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Pending Certificates</h3>
<a href="{{ url_for('admin.certificates', status='pending') }}"
class="text-sm text-blue-600 hover:text-blue-800">View All</a>
</div>
<div id="pending-certificates" class="space-y-3">
<div class="text-center text-gray-500 py-4">
<i class="fas fa-spinner fa-spin"></i>
Loading...
</div>
</div>
</div>
<!-- Recent Audit Logs -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Activity</h3>
</div>
<div class="space-y-3">
{% for log in recent_logs[:10] %}
<div class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fas fa-user text-blue-600 text-xs"></i>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-900">
<span class="font-medium">{{ log.user.username if log.user else 'System' }}</span>
{{ log.action }}
</p>
<p class="text-xs text-gray-500">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
loadPendingCertificates();
// Network control buttons
document.getElementById('start-network').addEventListener('click', function() {
controlFabricNetwork('start');
});
document.getElementById('stop-network').addEventListener('click', function() {
controlFabricNetwork('stop');
});
// Refresh stats button
document.getElementById('refresh-stats').addEventListener('click', function() {
location.reload();
});
});
function loadPendingCertificates() {
fetch('/admin/certificates/pending')
.then(response => response.json())
.then(data => {
const container = document.getElementById('pending-certificates');
if (data.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-4">No pending certificates</div>';
return;
}
container.innerHTML = data.slice(0, 5).map(cert =>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p class="text-sm font-medium text-gray-900">${cert.product_name}</p>
<p class="text-xs text-gray-500">by ${cert.supplier} • ${cert.days_pending} days ago</p>
</div>
<a href="/admin/certificates/${cert.id}"
class="text-blue-600 hover:text-blue-800 text-sm">Review</a>
</div>
).join('');
});
}
function controlFabricNetwork(action) {
showLoading();
fetch(/admin/blockchain/${action}-network, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert(Network ${action} completed successfully);
setTimeout(() => location.reload(), 2000);
} else {
alert(Failed to ${action} network: ${data.error});
}
})
.catch(error => {
hideLoading();
alert(Error: ${error.message});
});
}
</script>
{% endblock %}
DASHBOARD_MAIN_TEMPLATE
# User list template
cat > backend/app/admin/templates/users/list.html << 'USER_LIST_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}User Management{% endblock %}
{% block content %}
<!-- Header with search and filters -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">User Management</h2>
<div class="flex flex-col sm:flex-row gap-4">
<!-- Search -->
<div class="relative">
<input type="text" id="user-search" placeholder="Search users..."
value="{{ search }}"
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<!-- Role filter -->
<select id="role-filter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Roles</option>
<option value="user" {{ 'selected' if role_filter == 'user' }}>User</option>
<option value="admin" {{ 'selected' if role_filter == 'admin' }}>Admin</option>
<option value="super_admin" {{ 'selected' if role_filter == 'super_admin' }}>Super Admin</option>
</select>
<!-- Create user button -->
<a href="{{ url_for('admin.create_user') }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-plus mr-2"></i>Create User
</a>
</div>
</div>
</div>
<!-- Users table -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Certificates</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for user in users.items %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
{{ user.username[0].upper() }}
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.username }}</div>
<div class="text-sm text-gray-500">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-purple-100 text-purple-800' if user.role == 'super_admin'
else 'bg-blue-100 text-blue-800' if user.role == 'admin'
else 'bg-gray-100 text-gray-800' }}">
{{ user.role.replace('_', ' ').title() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if user.is_active else 'error' }}"></span>
<span class="text-sm text-gray-900">
{{ 'Active' if user.is_active else 'Inactive' }}
{% if user.is_verified %}
<i class="fas fa-check-circle text-green-500 ml-1" title="Verified"></i>
{% endif %}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ user.certificates.count() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="text-blue-600 hover:text-blue-900">View</a>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | lower }})"
class="text-{{ 'red' if user.is_active else 'green' }}-600 hover:text-{{ 'red' if user.is_active else 'green' }}-900">
{{ 'Deactivate' if user.is_active else 'Activate' }}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleUserStatus(userId, isActive) {
const action = isActive ? 'deactivate' : 'activate';
if (confirm(Are you sure you want to ${action} this user?)) {
showLoading();
fetch(/admin/users/${userId}/toggle-status, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
USER_LIST_TEMPLATE
# Create integration script
cat > integrate_admin.sh << 'INTEGRATE_ADMIN'
#!/bin/bash
set -e
echo "Integrating Admin Dashboard with Flask App..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# Update main Flask app to register admin blueprint
if ! grep -q "from backend.app.admin import bp as admin_bp" backend/app/__init__.py; then
cat >> backend/app/__init__.py << 'FLASK_ADMIN_INTEGRATION'
# Register admin blueprint
from backend.app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
# Import admin models for database creation
from backend.app.models import admin_models
FLASK_ADMIN_INTEGRATION
fi
# Update User model to include admin functionality
if ! grep -q "role = db.Column" backend/app/models/user.py; then
cat >> backend/app/models/user.py << 'USER_ADMIN_FIELDS'
# Admin fields
role = db.Column(db.String(20), default='user')
is_verified = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True)
last_login = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@property
def is_admin(self):
return self.role in ['admin', 'super_admin']
def has_role(self, role):
return self.role == role
USER_ADMIN_FIELDS
fi
# Update Certificate model with admin fields
if ! grep -q "status = db.Column" backend/app/models/certificate.py; then
cat >> backend/app/models/certificate.py << 'CERT_ADMIN_FIELDS'
# Admin approval fields
status = db.Column(db.String(20), default='pending') # pending, approved, rejected, revoked
approved_at = db.Column(db.DateTime)
approved_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejected_at = db.Column(db.DateTime)
rejected_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejection_reason = db.Column(db.Text)
revoked_at = db.Column(db.DateTime)
revoked_by = db.Column(db.Integer, db.ForeignKey('users.id'))
revocation_reason = db.Column(db.Text)
# QR Code path
qr_code_path = db.Column(db.String(255))
# Relationships
approver = db.relationship('User', foreign_keys=[approved_by], backref='approved_certificates')
rejecter = db.relationship('User', foreign_keys=[rejected_by], backref='rejected_certificates')
revoker = db.relationship('User', foreign_keys=[revoked_by], backref='revoked_certificates')
def generate_certificate_id(self):
"""Generate unique certificate ID"""
import uuid
import hashlib
unique_string = f"{self.product_name}_{self.user_id}_{uuid.uuid4().hex[:8]}"
self.certificate_id = hashlib.md5(unique_string.encode()).hexdigest()[:12].upper()
return self.certificate_id
CERT_ADMIN_FIELDS
fi
# Install required dependencies
echo "Installing additional dependencies..."
pip install psutil docker
# Create database migration for admin fields
echo "Creating database migration for admin dashboard..."
export FLASK_APP=backend/app
flask db migrate -m "Add admin dashboard fields"
flask db upgrade
# Create default admin user script
mkdir -p scripts
cat > scripts/create_admin.py << 'CREATE_ADMIN_SCRIPT'
#!/usr/bin/env python3
"""Create default admin user for NanoTrace"""
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend')))
from backend.app import create_app, db
from backend.app.models.user import User
import getpass
def create_admin():
app = create_app()
with app.app_context():
print("Creating NanoTrace Admin User")
print("=" * 30)
username = input("Admin username: ")
email = input("Admin email: ")
password = getpass.getpass("Admin password: ")
# Check if user already exists
existing_user = User.query.filter_by(username=username).first()
if existing_user:
print(f"User {username} already exists!")
return
existing_email = User.query.filter_by(email=email).first()
if existing_email:
print(f"Email {email} already exists!")
return
# Create admin user
admin_user = User(
username=username,
email=email,
role='super_admin',
is_verified=True,
is_active=True
)
admin_user.set_password(password)
db.session.add(admin_user)
db.session.commit()
print(f"Admin user {username} created successfully!")
print(f"You can now login at: http://your-domain/admin/dashboard")
if __name__ == '__main__':
create_admin()
CREATE_ADMIN_SCRIPT
chmod +x scripts/create_admin.py
# Create systemd service monitoring script
cat > scripts/monitor_services.py << 'SERVICE_MONITOR_SCRIPT'
#!/usr/bin/env python3
"""Monitor system services and record metrics"""
import sys
import os
import time
import subprocess
from datetime import datetime
# Add backend to path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend')))
from backend.app import create_app, db
from backend.app.admin.utils import SystemMonitor
def monitor_services():
"""Monitor services and record metrics"""
app = create_app()
with app.app_context():
try:
# Record system metrics
SystemMonitor.record_metrics()
# Check services
services = ['postgresql', 'redis-server', 'nginx', 'nanotrace']
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
print(f"{datetime.now()}: {service} - {status}")
except Exception as e:
print(f"{datetime.now()}: Error checking {service}: {e}")
print(f"{datetime.now()}: Monitoring cycle completed")
except Exception as e:
print(f"{datetime.now()}: Error in monitoring: {e}")
if __name__ == '__main__':
monitor_services()
SERVICE_MONITOR_SCRIPT
chmod +x scripts/monitor_services.py
# Create cron job for monitoring
echo "Setting up monitoring cron job..."
(crontab -l 2>/dev/null | grep -v nanotrace_monitor; echo "*/5 * * * * cd /home/michal/NanoTrace && /home/michal/NanoTrace/venv/bin/python scripts/monitor_services.py >> /var/log/nanotrace_monitor.log 2>&1") | crontab -
# Create admin CSS
cat > backend/app/admin/static/css/admin.css << 'ADMIN_CSS'
/* Custom admin dashboard styles */
.admin-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.metric-card {
transition: all 0.3s ease;
}
.metric-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.status-card {
border-left: 4px solid;
}
.status-card.healthy {
border-color: #10b981;
}
.status-card.warning {
border-color: #f59e0b;
}
.status-card.error {
border-color: #ef4444;
}
/* Loading animations */
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
/* Table enhancements */
.admin-table tbody tr:hover {
background-color: #f8fafc;
}
/* Chart containers */
.chart-container {
position: relative;
height: 300px;
}
/* Custom scrollbar */
.admin-scroll::-webkit-scrollbar {
width: 6px;
}
.admin-scroll::-webkit-scrollbar-track {
background: #f1f5f9;
}
.admin-scroll::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.admin-scroll::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Mobile responsive fixes */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
}
ADMIN_CSS
echo "Admin Dashboard Integration Complete!"
echo ""
echo "Next steps:"
echo "1. Run: python scripts/create_admin.py (to create admin user)"
echo "2. Restart your Flask application"
echo "3. Access admin dashboard at: http://your-domain/admin/dashboard"
echo "4. Monitor logs at: tail -f /var/log/nanotrace_monitor.log"
echo ""
echo "Admin Features Available:"
echo " 📊 System monitoring with real-time metrics"
echo " 👥 User management (create, edit, activate/deactivate)"
echo " 📄 Certificate approval workflow"
echo " 🔗 Blockchain network control"
echo " ⚙️ System configuration management"
echo " 📋 Audit logging and activity tracking"
echo " 💾 System maintenance utilities"
echo " 🔧 Service control and monitoring"
INTEGRATE_ADMIN
chmod +x integrate_admin.sh
echo "🎛️ Admin Dashboard Builder Complete!"
echo ""
echo "Generated Files:"
echo " 📁 backend/app/admin/ - Admin blueprint with views and templates"
echo " 📊 backend/app/models/admin_models.py - Admin data models"
echo " 🎨 backend/app/admin/templates/ - Professional admin UI templates"
echo " 🔧 scripts/create_admin.py - Admin user creation script"
echo " 📈 scripts/monitor_services.py - System monitoring script"
echo " 🔗 integrate_admin.sh - Integration script"
echo ""
echo "To complete setup, run:"
echo " ./integrate_admin.sh"
echo ""
echo "Admin Dashboard Features:"
echo " 🎯 Real-time system monitoring with charts"
echo " 👤 Complete user management system"
echo " 📋 Certificate approval workflow"
echo " ⛓️ Blockchain network management"
echo " 🔧 System configuration and maintenance"
echo " 📊 Analytics and reporting"
echo " 🔒 Audit logging and security"
echo " 💾 System monitoring and cleanup tools"
echo ""
echo "The dashboard provides a professional admin interface with:"
echo " • Modern responsive design with Tailwind CSS"
echo " • Real-time metrics and monitoring"
echo " • Role-based access control"
echo " • Comprehensive system management"
echo " • Mobile-friendly interface"
echo " • Secure audit logging"ChatGPT said:
Looking great—much tighter. A few blockers remain that will 500 or misbehave. Here are the precise fixes + drop-in stubs so the admin boots cleanly.
Must-fix items
1) FLASK_APP in integrate_admin.sh
Current line:
bashexport FLASK_APP=backend/app
This won’t find your factory. Change to:
bashexport FLASK_APP=backend.app:create_app
2) Include your admin CSS
Add this to <head> in backend/app/admin/templates/admin_base.html:
html<link rel="stylesheet" href="{{ url_for('admin.static', filename='css/admin.css') }}">
3) JS boolean in Users table
In backend/app/admin/templates/users/list.html change:
html<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | lower }})"
to:
html<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | tojson }})"
(|tojson ensures a real JS boolean.)
4) Missing templates (these will 500)
You render several templates that don’t exist yet. Drop these minimal stubs in so pages load:
backend/app/admin/templates/users/detail.html
html{% extends "admin_base.html" %}
{% block page_title %}User: {{ user.username }}{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold mb-4">Profile</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-500">Email:</span> {{ user.email }}</div>
<div><span class="text-gray-500">Role:</span> {{ user.role }}</div>
<div><span class="text-gray-500">Status:</span> {{ 'Active' if user.is_active else 'Inactive' }}</div>
<div><span class="text-gray-500">Verified:</span> {{ 'Yes' if user.is_verified else 'No' }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h4 class="font-semibold mb-3">Recent Certificates</h4>
<ul class="space-y-2 text-sm">
{% for c in certificates %}
<li class="flex justify-between border p-2 rounded">
<span>{{ c.product_name }}</span>
<span class="text-gray-500">{{ c.status|title }}</span>
</li>
{% else %}
<li class="text-gray-500">No certificates.</li>
{% endfor %}
</ul>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h4 class="font-semibold mb-3">Recent Activity</h4>
<ul class="space-y-2 text-sm">
{% for log in audit_logs %}
<li class="border p-2 rounded">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') }} — {{ log.action }}</li>
{% else %}
<li class="text-gray-500">No recent activity.</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
backend/app/admin/templates/users/create.html
html{% extends "admin_base.html" %}
{% block page_title %}Create User{% endblock %}
{% block content %}
<form method="post" class="bg-white rounded-xl p-6 shadow-sm border space-y-4">
<div>
<label class="block text-sm text-gray-600 mb-1">Username</label>
<input name="username" required class="w-full border rounded p-2">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Email</label>
<input name="email" type="email" required class="w-full border rounded p-2">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Password</label>
<input name="password" type="password" required class="w-full border rounded p-2">
</div>
<div class="flex gap-4">
<label class="inline-flex items-center gap-2">
<input type="checkbox" name="is_verified" value="true"> Verified
</label>
<label class="inline-flex items-center gap-2">
<input type="checkbox" name="is_active" value="true" checked> Active
</label>
<select name="role" class="border rounded p-2">
<option value="user" selected>User</option>
<option value="admin">Admin</option>
<option value="super_admin">Super Admin</option>
</select>
</div>
<button class="px-4 py-2 bg-blue-600 text-white rounded">Create</button>
</form>
{% endblock %}
backend/app/admin/templates/certificates/list.html
html{% extends "admin_base.html" %}
{% block page_title %}Certificates{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border">
<div class="flex justify-between mb-4">
<div class="font-semibold">Stats: Total {{ stats.total }}, Pending {{ stats.pending }}, Approved {{ stats.approved }}, Rejected {{ stats.rejected }}</div>
<form method="get" class="flex gap-2">
<input class="border rounded p-2" name="search" value="{{ search }}" placeholder="Search">
<select class="border rounded p-2" name="status">
<option value="">All</option>
{% for st in ['pending','approved','rejected','revoked'] %}
<option value="{{ st }}" {{ 'selected' if status_filter==st }}>{{ st|title }}</option>
{% endfor %}
</select>
<button class="px-3 py-2 bg-gray-800 text-white rounded">Filter</button>
</form>
</div>
<table class="min-w-full text-sm">
<thead><tr class="text-left border-b"><th class="py-2">Product</th><th>Status</th><th>User</th><th>Created</th></tr></thead>
<tbody>
{% for c in certificates.items %}
<tr class="border-b">
<td class="py-2"><a class="text-blue-600" href="{{ url_for('admin.certificate_detail', cert_id=c.id) }}">{{ c.product_name }}</a></td>
<td>{{ c.status|title }}</td>
<td>{{ c.user.username }}</td>
<td>{{ c.created_at.strftime('%Y-%m-%d') if c.created_at }}</td>
</tr>
{% else %}
<tr><td class="py-4 text-gray-500" colspan="4">No certificates.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
backend/app/admin/templates/certificates/detail.html
html{% extends "admin_base.html" %}
{% block page_title %}Certificate #{{ certificate.id }}{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-500">Product:</span> {{ certificate.product_name }}</div>
<div><span class="text-gray-500">Material:</span> {{ certificate.material_type }}</div>
<div><span class="text-gray-500">User:</span> {{ certificate.user.username }}</div>
<div><span class="text-gray-500">Status:</span> {{ certificate.status|title }}</div>
</div>
{% if certificate.status == 'pending' %}
<div class="flex gap-3">
<button id="approve" class="px-4 py-2 bg-green-600 text-white rounded">Approve</button>
</div>
<script>
document.getElementById('approve').onclick = () => {
fetch('{{ url_for("admin.approve_certificate", cert_id=certificate.id) }}', {method:'POST'})
.then(r=>r.json()).then(j=>{ if(j.success){ location.reload() } else { alert(j.error) }});
}
</script>
{% endif %}
</div>
{% endblock %}
backend/app/admin/templates/blockchain/status.html
html{% extends "admin_base.html" %}
{% block page_title %}Blockchain{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border space-y-3">
<div class="flex gap-2">
<button onclick="ctl('start')" class="px-3 py-1 bg-green-600 text-white rounded">Start</button>
<button onclick="ctl('stop')" class="px-3 py-1 bg-red-600 text-white rounded">Stop</button>
</div>
<div class="grid gap-2 mt-4">
{% for name, s in fabric_status.items() %}
<div class="flex justify-between border rounded p-2">
<div>{{ name }}</div>
<div class="{{ 'text-green-600' if s.healthy else 'text-red-600' }}">{{ s.status }}</div>
</div>
{% else %}
<div class="text-gray-500">No Fabric containers found.</div>
{% endfor %}
</div>
</div>
<script>
function ctl(action){
showLoading();
fetch(`/admin/blockchain/${action}-network`, {method:'POST'}).then(r=>r.json()).then(j=>{
hideLoading(); alert(j.success? ('OK: '+j.message): ('ERR: '+j.error)); if(j.success) location.reload();
}).catch(e=>{hideLoading(); alert(e.message)});
}
</script>
{% endblock %}
backend/app/admin/templates/system/overview.html
html{% extends "admin_base.html" %}
{% block page_title %}System Overview{% endblock %}
{% block content %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h3 class="font-semibold mb-4">Resources</h3>
<div>CPU: {{ '%.1f'|format(system_stats.cpu.percent) }}%</div>
<div>Memory: {{ '%.1f'|format(system_stats.memory.percent) }}%</div>
<div>Disk: {{ '%.1f'|format(system_stats.disk.percent) }}%</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h3 class="font-semibold mb-4">Config</h3>
<ul class="text-sm">
{% for k,v in config_dict.items() %}
<li class="border-b py-1"><span class="text-gray-500">{{ k }}</span>: {{ v }}</li>
{% else %}
<li class="text-gray-500">No config entries.</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
backend/app/admin/templates/system/services.html
html{% extends "admin_base.html" %}
{% block page_title %}Services{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border">
<table class="min-w-full text-sm">
<thead><tr class="text-left border-b"><th class="py-2">Service</th><th>Status</th></tr></thead>
<tbody>
{% for name, s in services.items() %}
<tr class="border-b">
<td class="py-2">{{ name }}</td>
<td class="{{ 'text-green-600' if s.active else 'text-red-600' }}">{{ s.status }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
5) Add a certificate detail route (the dashboard links to it)
In backend/app/admin/views/certificates.py, add:
python@bp.route('/certificates/<int:cert_id>')
@admin_required
def certificate_detail(cert_id):
cert = Certificate.query.get_or_404(cert_id)
log_admin_action("Viewed certificate details", "certificate", cert_id)
return render_template('certificates/detail.html', certificate=cert)
Nice-to-have (but recommended)
-
Cron log file permissions:
bashsudo touch /var/log/nanotrace_monitor.log sudo chown $(whoami):$(id -gn) /var/log/nanotrace_monitor.log -
Remove unused imports in a later pass (
requests,time, etc.) to keep things lean.
Smoke test checklist
-
./integrate_admin.sh -
python scripts/create_admin.py(create a super admin) -
Restart your service and browse to
/admin/dashboard. -
Click through: Users → (View), Certificates → (row link), Blockchain, System → Services.
Ping me with any traceback lines if something still hiccups, and I’ll zero in fast.
You said:
where to add detail.html:# User list template
cat > backend/app/admin/templates/users/list.html << 'USER_LIST_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}User Management{% endblock %}
{% block content %}
<!-- Header with search and filters -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">User Management</h2>
<div class="flex flex-col sm:flex-row gap-4">
<!-- Search -->
<div class="relative">
<input type="text" id="user-search" placeholder="Search users..."
value="{{ search }}"
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<!-- Role filter -->
<select id="role-filter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Roles</option>
<option value="user" {{ 'selected' if role_filter == 'user' }}>User</option>
<option value="admin" {{ 'selected' if role_filter == 'admin' }}>Admin</option>
<option value="super_admin" {{ 'selected' if role_filter == 'super_admin' }}>Super Admin</option>
</select>
<!-- Create user button -->
<a href="{{ url_for('admin.create_user') }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-plus mr-2"></i>Create User
</a>
</div>
</div>
</div>
<!-- Users table -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Certificates</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for user in users.items %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
{{ user.username[0].upper() }}
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.username }}</div>
<div class="text-sm text-gray-500">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-purple-100 text-purple-800' if user.role == 'super_admin'
else 'bg-blue-100 text-blue-800' if user.role == 'admin'
else 'bg-gray-100 text-gray-800' }}">
{{ user.role.replace('_', ' ').title() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if user.is_active else 'error' }}"></span>
<span class="text-sm text-gray-900">
{{ 'Active' if user.is_active else 'Inactive' }}
{% if user.is_verified %}
<i class="fas fa-check-circle text-green-500 ml-1" title="Verified"></i>
{% endif %}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ user.certificates.count() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="text-blue-600 hover:text-blue-900">View</a>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | tojson }})"
class="text-{{ 'red' if user.is_active else 'green' }}-600 hover:text-{{ 'red' if user.is_active else 'green' }}-900">
{{ 'Deactivate' if user.is_active else 'Activate' }}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
ChatGPT said:
Good question. The list.html you showed lives here:
swiftbackend/app/admin/templates/users/list.html
That template has a link:
jinja2<a href="{{ url_for('admin.user_detail', user_id=user.id) }}">View</a>
→ which calls the route user_detail in backend/app/admin/views/users.py.
That route renders:
pythonreturn render_template('users/detail.html', user=user, certificates=..., audit_logs=...)
So you need to add detail.html inside the same users/ subfolder where list.html already lives:
swiftbackend/app/admin/templates/users/detail.html
Steps in your terminal:
bashcd ~/NanoTrace/backend/app/admin/templates/users
nano detail.html
Paste in the stub I gave earlier, for example:
html{% extends "admin_base.html" %}
{% block page_title %}User: {{ user.username }}{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold mb-4">Profile</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-500">Email:</span> {{ user.email }}</div>
<div><span class="text-gray-500">Role:</span> {{ user.role }}</div>
<div><span class="text-gray-500">Status:</span> {{ 'Active' if user.is_active else 'Inactive' }}</div>
<div><span class="text-gray-500">Verified:</span> {{ 'Yes' if user.is_verified else 'No' }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h4 class="font-semibold mb-3">Recent Certificates</h4>
<ul class="space-y-2 text-sm">
{% for c in certificates %}
<li class="flex justify-between border p-2 rounded">
<span>{{ c.product_name }}</span>
<span class="text-gray-500">{{ c.status|title }}</span>
</li>
{% else %}
<li class="text-gray-500">No certificates.</li>
{% endfor %}
</ul>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h4 class="font-semibold mb-3">Recent Activity</h4>
<ul class="space-y-2 text-sm">
{% for log in audit_logs %}
<li class="border p-2 rounded">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') }} — {{ log.action }}</li>
{% else %}
<li class="text-gray-500">No recent activity.</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
✅ With this, the “View” link in your user list will resolve to detail.html properly.
Do you want me to also drop stubs for the other missing templates (like users/create.html, certificates/list.html, etc.) into the right folders so you won’t hit TemplateNotFound?
You said:
i did not run the script yet so i want have those directories
ChatGPT said:
Totally fine — if you haven’t run the script yet, just create the folders yourself and drop the file in.
Where it goes
Put detail.html here (same place as list.html will live):
swiftbackend/app/admin/templates/users/detail.html
Make the folders + file (commands)
bashmkdir -p backend/app/admin/templates/users
# create the template
cat > backend/app/admin/templates/users/detail.html << 'HTML'
{% extends "admin_base.html" %}
{% block page_title %}User: {{ user.username }}{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold mb-4">Profile</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-500">Email:</span> {{ user.email }}</div>
<div><span class="text-gray-500">Role:</span> {{ user.role }}</div>
<div><span class="text-gray-500">Status:</span> {{ 'Active' if user.is_active else 'Inactive' }}</div>
<div><span class="text-gray-500">Verified:</span> {{ 'Yes' if user.is_verified else 'No' }}</div>
<div><span class="text-gray-500">Joined:</span> {{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}</div>
<div><span class="text-gray-500">Last login:</span> {{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else 'Never' }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h4 class="font-semibold mb-3">Recent Certificates</h4>
<ul class="space-y-2 text-sm">
{% for c in certificates %}
<li class="flex justify-between border p-2 rounded">
<span>{{ c.product_name }}</span>
<span class="text-gray-500">{{ c.status|title }}</span>
</li>
{% else %}
<li class="text-gray-500">No certificates.</li>
{% endfor %}
</ul>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border">
<h4 class="font-semibold mb-3">Recent Activity</h4>
<ul class="space-y-2 text-sm">
{% for log in audit_logs %}
<li class="border p-2 rounded">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') }} — {{ log.action }}</li>
{% else %}
<li class="text-gray-500">No recent activity.</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
HTML
Small routing note
Your “View” link points to:
jinja2url_for('admin.user_detail', user_id=user.id)
and the route renders:
pythonrender_template('users/detail.html', ...)
So the path above is correct relative to the admin blueprint’s template_folder='templates'.
If you’d like, I can also give you quick stubs for users/create.html and users/edit.html so you won’t hit TemplateNotFound later.
You said:
can i run the script to build admin dash
ChatGPT said:
Yes, you can — but do it in the right order so you don’t get halfway and break your working service. Here’s the safe flow for you:
1. Make sure your app is running clean
Before running the integrate_admin.sh script:
bashcd ~/NanoTrace
source venv/bin/activate
export PYTHONPATH=/home/michal/NanoTrace
python - <<'PY'
from backend.wsgi import app
print("App loads fine:", app)
PY
✅ If this prints the Flask app without errors, you’re good to continue.
2. Run the integration script
bashcd ~/NanoTrace
chmod +x integrate_admin.sh
./integrate_admin.sh
This script will:
-
Create the admin blueprint (views, templates, static, models).
-
Append blueprint registration to
backend/app/__init__.py. -
Add admin fields to
UserandCertificatemodels (if missing). -
Install extra packages (
psutil,docker). -
Run
flask db migrate+flask db upgradeto add DB columns. -
Add a monitoring cron job.
-
Drop helper scripts (
scripts/create_admin.pyandscripts/monitor_services.py).
3. Create your first super admin
Run the helper:
bashcd ~/NanoTrace
source venv/bin/activate
python scripts/create_admin.py
Enter username, email, and password. This becomes your login for /admin/dashboard.
4. Restart your Gunicorn service
bashsudo systemctl daemon-reexec sudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
5. Test it
-
Go to:
https://nanotrace.org/admin/dashboard -
Log in with the admin you just created.
⚠️ A couple of notes before you hit run:
-
This will alter your DB schema — make sure you don’t have critical data you’d lose.
-
If migrations fail, I can guide you to manually fix them.
-
The admin dashboard is protected — you must be logged in as an admin to access it.
Do you want me to also give you the exact flask db migrate/upgrade commands to test manually before running the big script, so you’re 100% safe?
You said:
#!/bin/bash
# =============================================================================
# NanoTrace Admin Dashboard Builder - Complete Fixed Version
# Creates a sophisticated Flask admin dashboard for system management
# =============================================================================
set -e
echo "Building NanoTrace Admin Dashboard..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# Create admin blueprint structure
mkdir -p backend/app/admin/{views,templates,static}
mkdir -p backend/app/admin/static/{css,js}
mkdir -p backend/app/admin/templates/{base,dashboard,users,certificates,blockchain,system}
# 1. Create Admin Blueprint
cat > backend/app/admin/__init__.py << 'ADMIN_INIT'
from flask import Blueprint
bp = Blueprint('admin', __name__,
template_folder='templates',
static_folder='static',
static_url_path='/admin/static')
from backend.app.admin.views import main, users, certificates, blockchain, system
ADMIN_INIT
# 2. Create Admin Models
cat > backend/app/models/admin_models.py << 'ADMIN_MODELS'
from datetime import datetime
from backend.app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import enum
class UserRole(enum.Enum):
USER = "user"
ADMIN = "admin"
SUPER_ADMIN = "super_admin"
class SystemMetric(db.Model):
"""Track system metrics and health"""
__tablename__ = 'system_metrics'
id = db.Column(db.Integer, primary_key=True)
metric_name = db.Column(db.String(100), nullable=False)
metric_value = db.Column(db.Float, nullable=False)
metric_unit = db.Column(db.String(20))
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.metric_name,
'value': self.metric_value,
'unit': self.metric_unit,
'timestamp': self.timestamp.isoformat()
}
class AuditLog(db.Model):
"""Track admin actions for security"""
__tablename__ = 'audit_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
action = db.Column(db.String(200), nullable=False)
resource = db.Column(db.String(100))
resource_id = db.Column(db.String(100))
ip_address = db.Column(db.String(45))
user_agent = db.Column(db.Text)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref=db.backref('audit_logs', lazy=True))
def to_dict(self):
return {
'id': self.id,
'user': self.user.username if self.user else 'Unknown',
'action': self.action,
'resource': self.resource,
'resource_id': self.resource_id,
'ip': self.ip_address,
'timestamp': self.timestamp.isoformat()
}
class SystemConfig(db.Model):
"""Store system configuration settings"""
__tablename__ = 'system_config'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.Text)
description = db.Column(db.Text)
updated_by = db.Column(db.Integer, db.ForeignKey('users.id'))
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
updater = db.relationship('User')
@staticmethod
def get_config(key, default=None):
config = SystemConfig.query.filter_by(key=key).first()
return config.value if config else default
@staticmethod
def set_config(key, value, user_id, description=None):
config = SystemConfig.query.filter_by(key=key).first()
if config:
config.value = value
config.updated_by = user_id
config.updated_at = datetime.utcnow()
else:
config = SystemConfig(
key=key,
value=value,
description=description,
updated_by=user_id
)
db.session.add(config)
db.session.commit()
return config
ADMIN_MODELS
# 3. Create Admin Utils
cat > backend/app/admin/utils.py << 'ADMIN_UTILS'
from functools import wraps
from flask import abort, request, current_app
from flask_login import current_user
import psutil
import time
import requests
from datetime import datetime, timedelta
from backend.app.models.admin_models import SystemMetric, AuditLog
from backend.app import db
def admin_required(f):
"""Decorator to require admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role not in ['admin', 'super_admin']:
abort(403)
return f(*args, **kwargs)
return decorated_function
def super_admin_required(f):
"""Decorator to require super admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != 'super_admin':
abort(403)
return f(*args, **kwargs)
return decorated_function
def log_admin_action(action, resource=None, resource_id=None):
"""Log admin actions for audit trail"""
try:
log_entry = AuditLog(
user_id=current_user.id,
action=action,
resource=resource,
resource_id=str(resource_id) if resource_id else None,
ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent', '')[:500]
)
db.session.add(log_entry)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Failed to log admin action: {e}")
class SystemMonitor:
"""System monitoring utilities"""
@staticmethod
def get_system_stats():
"""Get current system statistics"""
try:
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
# Network stats (simplified)
network = psutil.net_io_counters()
return {
'cpu': {
'percent': cpu_percent,
'count': psutil.cpu_count()
},
'memory': {
'total': memory.total,
'available': memory.available,
'percent': memory.percent,
'used': memory.used
},
'disk': {
'total': disk.total,
'used': disk.used,
'free': disk.free,
'percent': (disk.used / disk.total) * 100
},
'network': {
'bytes_sent': network.bytes_sent,
'bytes_recv': network.bytes_recv
},
'timestamp': datetime.utcnow().isoformat()
}
except Exception as e:
current_app.logger.error(f"Error getting system stats: {e}")
return {}
@staticmethod
def record_metrics():
"""Record system metrics to database"""
stats = SystemMonitor.get_system_stats()
if not stats:
return
try:
metrics = [
SystemMetric(metric_name='cpu_percent', metric_value=stats['cpu']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_percent', metric_value=stats['memory']['percent'], metric_unit='%'),
SystemMetric(metric_name='disk_percent', metric_value=stats['disk']['percent'], metric_unit='%'),
SystemMetric(metric_name='memory_used', metric_value=stats['memory']['used'], metric_unit='bytes'),
SystemMetric(metric_name='disk_used', metric_value=stats['disk']['used'], metric_unit='bytes'),
]
for metric in metrics:
db.session.add(metric)
db.session.commit()
except Exception as e:
current_app.logger.error(f"Error recording metrics: {e}")
db.session.rollback()
@staticmethod
def check_fabric_network():
"""Check Hyperledger Fabric network status"""
try:
# Check if Fabric containers are running
import docker
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com'
]
container_status = {}
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container_status[container_name] = {
'status': container.status,
'healthy': container.status == 'running'
}
except:
container_status[container_name] = {
'status': 'not_found',
'healthy': False
}
return container_status
except Exception as e:
current_app.logger.error(f"Error checking Fabric network: {e}")
return {}
@staticmethod
def get_database_stats():
"""Get database statistics"""
try:
from backend.app.models.user import User
from backend.app.models.certificate import Certificate
stats = {
'total_users': User.query.count(),
'admin_users': User.query.filter_by(role='admin').count(),
'total_certificates': Certificate.query.count(),
'pending_certificates': Certificate.query.filter_by(status='pending').count(),
'approved_certificates': Certificate.query.filter_by(status='approved').count(),
'rejected_certificates': Certificate.query.filter_by(status='rejected').count(),
}
return stats
except Exception as e:
current_app.logger.error(f"Error getting database stats: {e}")
return {}
def format_bytes(bytes):
"""Format bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes < 1024.0:
return f"{bytes:.1f} {unit}"
bytes /= 1024.0
return f"{bytes:.1f} PB"
def format_uptime(seconds):
"""Format uptime in human readable format"""
days = seconds // 86400
hours = (seconds % 86400) // 3600
minutes = (seconds % 3600) // 60
parts = []
if days:
parts.append(f"{int(days)}d")
if hours:
parts.append(f"{int(hours)}h")
if minutes:
parts.append(f"{int(minutes)}m")
return " ".join(parts) if parts else "0m"
ADMIN_UTILS
# 4. Create Views Directory Structure
mkdir -p backend/app/admin/views
# 5. Create Main Admin Views
cat > backend/app/admin/views/main.py << 'ADMIN_MAIN_VIEWS'
from flask import render_template, jsonify, request, current_app
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, SystemMonitor, log_admin_action
from backend.app.models.admin_models import SystemMetric, AuditLog
from datetime import datetime, timedelta
from sqlalchemy import func
@bp.route('/dashboard')
@admin_required
def dashboard():
"""Main admin dashboard"""
log_admin_action("Accessed admin dashboard")
# Get system stats
system_stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
fabric_status = SystemMonitor.check_fabric_network()
# Get recent metrics for charts (last 24 hours)
since = datetime.utcnow() - timedelta(hours=24)
recent_metrics = SystemMetric.query.filter(
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.desc()).limit(100).all()
# Get recent audit logs
recent_logs = AuditLog.query.order_by(
AuditLog.timestamp.desc()
).limit(20).all()
return render_template('dashboard/main.html',
system_stats=system_stats,
db_stats=db_stats,
fabric_status=fabric_status,
recent_metrics=recent_metrics,
recent_logs=recent_logs)
@bp.route('/api/system-stats')
@admin_required
def api_system_stats():
"""API endpoint for real-time system stats"""
stats = SystemMonitor.get_system_stats()
db_stats = SystemMonitor.get_database_stats()
return jsonify({
'system': stats,
'database': db_stats,
'timestamp': datetime.utcnow().isoformat()
})
@bp.route('/api/metrics')
@admin_required
def api_metrics():
"""API endpoint for metrics data"""
hours = request.args.get('hours', 24, type=int)
metric_name = request.args.get('metric', 'cpu_percent')
since = datetime.utcnow() - timedelta(hours=hours)
metrics = SystemMetric.query.filter(
SystemMetric.metric_name == metric_name,
SystemMetric.timestamp >= since
).order_by(SystemMetric.timestamp.asc()).all()
data = [{
'timestamp': metric.timestamp.isoformat(),
'value': metric.metric_value
} for metric in metrics]
return jsonify(data)
@bp.route('/health-check')
@admin_required
def health_check():
"""Comprehensive system health check"""
log_admin_action("Performed health check")
health_status = {
'timestamp': datetime.utcnow().isoformat(),
'overall': 'healthy',
'checks': {}
}
# Database check
try:
from backend.app import db
db.session.execute('SELECT 1').fetchone()
health_status['checks']['database'] = {
'status': 'healthy',
'message': 'Database connection successful'
}
except Exception as e:
health_status['checks']['database'] = {
'status': 'unhealthy',
'message': f'Database error: {str(e)}'
}
health_status['overall'] = 'degraded'
# Fabric network check
fabric_status = SystemMonitor.check_fabric_network()
all_healthy = all(container['healthy'] for container in fabric_status.values())
health_status['checks']['fabric'] = {
'status': 'healthy' if all_healthy else 'unhealthy',
'message': f'Fabric containers: {len([c for c in fabric_status.values() if c["healthy"]])}/{len(fabric_status)} healthy',
'details': fabric_status
}
if not all_healthy:
health_status['overall'] = 'degraded'
return jsonify(health_status)
ADMIN_MAIN_VIEWS
# 6. Create User Management Views
cat > backend/app/admin/views/users.py << 'ADMIN_USER_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, super_admin_required, log_admin_action
from backend.app.models.user import User
from backend.app import db
from datetime import datetime
from werkzeug.security import generate_password_hash
@bp.route('/users')
@admin_required
def users():
"""User management page"""
log_admin_action("Accessed user management")
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
role_filter = request.args.get('role', '')
query = User.query
if search:
query = query.filter(
db.or_(
User.username.contains(search),
User.email.contains(search)
)
)
if role_filter:
query = query.filter(User.role == role_filter)
users = query.paginate(
page=page, per_page=25, error_out=False
)
return render_template('users/list.html',
users=users,
search=search,
role_filter=role_filter)
@bp.route('/users/<int:user_id>')
@admin_required
def user_detail(user_id):
"""User detail page"""
user = User.query.get_or_404(user_id)
log_admin_action("Viewed user details", "user", user_id)
# Get user's certificates
certificates = user.certificates.order_by('created_at desc').limit(10).all()
# Get user's audit logs
audit_logs = user.audit_logs.order_by('timestamp desc').limit(20).all()
return render_template('users/detail.html',
user=user,
certificates=certificates,
audit_logs=audit_logs)
@bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
def toggle_user_status(user_id):
"""Toggle user active status"""
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
try:
db.session.commit()
action = "activated" if user.is_active else "deactivated"
log_admin_action(f"User {action}", "user", user_id)
return jsonify({
'success': True,
'message': f'User {action} successfully',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Create new user"""
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
try:
user = User(
username=data['username'],
email=data['email'],
role=data.get('role', 'user'),
is_verified=data.get('is_verified', False) == 'true',
is_active=data.get('is_active', True) == 'true'
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
log_admin_action("Created user", "user", user.id)
if request.is_json:
return jsonify({
'success': True,
'message': 'User created successfully',
'user_id': user.id
})
else:
flash('User created successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user.id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error creating user: {str(e)}', 'error')
return render_template('users/create.html')
ADMIN_USER_VIEWS
# 7. Create Certificate Management Views
cat > backend/app/admin/views/certificates.py << 'ADMIN_CERT_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, log_admin_action
from backend.app.models.certificate import Certificate
from backend.app.models.user import User
from backend.app import db
from datetime import datetime, timedelta
@bp.route('/certificates')
@admin_required
def certificates():
"""Certificate management page"""
log_admin_action("Accessed certificate management")
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
search = request.args.get('search', '')
query = Certificate.query
if status_filter:
query = query.filter(Certificate.status == status_filter)
if search:
query = query.join(User).filter(
db.or_(
Certificate.product_name.contains(search),
Certificate.material_type.contains(search),
User.username.contains(search)
)
)
certificates = query.order_by(
Certificate.created_at.desc()
).paginate(page=page, per_page=25, error_out=False)
# Get statistics
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count()
}
return render_template('certificates/list.html',
certificates=certificates,
stats=stats,
status_filter=status_filter,
search=search)
@bp.route('/certificates/<int:cert_id>/approve', methods=['POST'])
@admin_required
def approve_certificate(cert_id):
"""Approve certificate"""
certificate = Certificate.query.get_or_404(cert_id)
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'approved'
certificate.approved_at = datetime.utcnow()
certificate.approved_by = current_user.id
# Generate certificate ID
if not certificate.certificate_id:
certificate.generate_certificate_id()
db.session.commit()
log_admin_action("Approved certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate approved successfully',
'certificate_id': certificate.certificate_id
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/pending')
@admin_required
def pending_certificates():
"""Get pending certificates for approval queue"""
certificates = Certificate.query.filter_by(
status='pending'
).order_by(Certificate.created_at.asc()).all()
return jsonify([{
'id': cert.id,
'product_name': cert.product_name,
'material_type': cert.material_type,
'supplier': cert.user.username,
'created_at': cert.created_at.isoformat(),
'days_pending': (datetime.utcnow() - cert.created_at).days
} for cert in certificates])
ADMIN_CERT_VIEWS
# 8. Create Blockchain Management Views
cat > backend/app/admin/views/blockchain.py << 'ADMIN_BLOCKCHAIN_VIEWS'
from flask import render_template, request, jsonify, current_app
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, log_admin_action, SystemMonitor
import docker
import subprocess
import os
@bp.route('/blockchain')
@admin_required
def blockchain_status():
"""Blockchain network status page"""
log_admin_action("Accessed blockchain management")
# Get Fabric network status
fabric_status = SystemMonitor.check_fabric_network()
return render_template('blockchain/status.html',
fabric_status=fabric_status)
@bp.route('/blockchain/start-network', methods=['POST'])
@admin_required
def start_fabric_network():
"""Start Hyperledger Fabric network"""
try:
fabric_path = "/home/michal/NanoTrace/fabric-network"
script_path = os.path.join(fabric_path, "network/scripts/start_network.sh")
if not os.path.exists(script_path):
return jsonify({
'success': False,
'error': 'Fabric network script not found'
}), 400
# Run the start script
result = subprocess.run([script_path],
capture_output=True,
text=True,
timeout=300) # 5 minute timeout
log_admin_action("Started Fabric network")
if result.returncode == 0:
return jsonify({
'success': True,
'message': 'Fabric network started successfully',
'output': result.stdout
})
else:
return jsonify({
'success': False,
'error': f'Failed to start network: {result.stderr}'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'Error starting network: {str(e)}'
}), 500
@bp.route('/blockchain/stop-network', methods=['POST'])
@admin_required
def stop_fabric_network():
"""Stop Hyperledger Fabric network"""
try:
client = docker.from_env()
fabric_containers = [
'peer0.org1.nanotrace.com',
'orderer.nanotrace.com',
'ca.org1.nanotrace.com',
'cli'
]
stopped_containers = []
for container_name in fabric_containers:
try:
container = client.containers.get(container_name)
container.stop()
stopped_containers.append(container_name)
except docker.errors.NotFound:
pass # Container already stopped
log_admin_action("Stopped Fabric network")
return jsonify({
'success': True,
'message': f'Stopped {len(stopped_containers)} containers',
'stopped': stopped_containers
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Error stopping network: {str(e)}'
}), 500
ADMIN_BLOCKCHAIN_VIEWS
# 9. Create System Management Views
cat > backend/app/admin/views/system.py << 'ADMIN_SYSTEM_VIEWS'
from flask import render_template, request, jsonify, current_app
from flask_login import current_user
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, super_admin_required, log_admin_action, SystemMonitor, format_bytes
from backend.app.models.admin_models import SystemConfig, SystemMetric
from backend.app import db
from datetime import datetime, timedelta
import os
import subprocess
import shutil
@bp.route('/system')
@admin_required
def system_overview():
"""System overview page"""
log_admin_action("Accessed system management")
system_stats = SystemMonitor.get_system_stats()
# Get system configuration
configs = SystemConfig.query.all()
config_dict = {config.key: config.value for config in configs}
# Get disk usage for important directories
directories = [
'/home/michal/NanoTrace',
'/var/log',
'/tmp'
]
disk_usage = {}
for directory in directories:
if os.path.exists(directory):
try:
usage = shutil.disk_usage(directory)
disk_usage[directory] = {
'total': usage.total,
'used': usage.used,
'free': usage.free,
'total_formatted': format_bytes(usage.total),
'used_formatted': format_bytes(usage.used),
'free_formatted': format_bytes(usage.free)
}
except:
disk_usage[directory] = None
return render_template('system/overview.html',
system_stats=system_stats,
config_dict=config_dict,
disk_usage=disk_usage)
@bp.route('/system/services')
@admin_required
def system_services():
"""View system services status"""
services = [
'postgresql',
'redis-server',
'nginx',
'nanotrace' # Our Flask app service
]
service_status = {}
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
service_status[service] = {
'status': status,
'active': status == 'active'
}
except Exception as e:
service_status[service] = {
'status': 'error',
'active': False,
'error': str(e)
}
return render_template('system/services.html',
services=service_status)
ADMIN_SYSTEM_VIEWS
# 10. Create Admin Templates
# Base template
cat > backend/app/admin/templates/admin_base.html << 'ADMIN_BASE_TEMPLATE'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}NanoTrace Admin Dashboard{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('admin.static', filename='css/admin.css') }}">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.sidebar-transition {
transition: all 0.3s ease-in-out;
}
.admin-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-healthy { background-color: #10b981; }
.status-warning { background-color: #f59e0b; }
.status-error { background-color: #ef4444; }
.metric-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
</style>
</head>
<body class="bg-gray-100">
<div class="flex min-h-screen">
<!-- Sidebar -->
<aside class="bg-gray-900 text-white w-64 min-h-screen sidebar-transition" id="sidebar">
<div class="p-4">
<div class="flex items-center mb-8">
<i class="fas fa-cog text-2xl mr-3 text-blue-400"></i>
<h1 class="text-xl font-bold">Admin Panel</h1>
</div>
<nav class="space-y-2">
<a href="{{ url_for('admin.dashboard') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-tachometer-alt mr-3"></i>
Dashboard
</a>
<a href="{{ url_for('admin.users') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-users mr-3"></i>
Users
</a>
<a href="{{ url_for('admin.certificates') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-certificate mr-3"></i>
Certificates
</a>
<a href="{{ url_for('admin.blockchain_status') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-link mr-3"></i>
Blockchain
</a>
<a href="{{ url_for('admin.system_overview') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-server mr-3"></i>
System
</a>
<hr class="my-4 border-gray-700">
<a href="{{ url_for('main.index') }}"
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-home mr-3"></i>
Back to Site
</a>
<a href="{{ url_for('auth.logout') }}"
class="flex items-center px-4 py-2 text-red-400 hover:bg-red-800 hover:text-white rounded-lg transition-colors">
<i class="fas fa-sign-out-alt mr-3"></i>
Logout
</a>
</nav>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-hidden">
<!-- Top Header -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex items-center">
<button id="sidebar-toggle" class="text-gray-500 hover:text-gray-700 mr-4">
<i class="fas fa-bars"></i>
</button>
<h2 class="text-xl font-semibold text-gray-800">{% block page_title %}Dashboard{% endblock %}</h2>
</div>
<div class="flex items-center space-x-4">
<!-- System Status Indicator -->
<div class="flex items-center">
<span class="status-indicator status-healthy" id="system-status"></span>
<span class="text-sm text-gray-600">System Status</span>
</div>
<!-- User Info -->
<div class="flex items-center">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium mr-2">
{{ current_user.username[0].upper() }}
</div>
<span class="text-sm text-gray-700">{{ current_user.username }}</span>
</div>
</div>
</div>
</header>
<!-- Page Content -->
<div class="p-6 overflow-y-auto" style="height: calc(100vh - 73px);">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mb-4">
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} bg-{{ 'green' if category == 'success' else 'red' }}-100 border border-{{ 'green' if category == 'success' else 'red' }}-400 text-{{ 'green' if category == 'success' else 'red' }}-700 px-4 py-3 rounded mb-2">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 flex items-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<span>Loading...</span>
</div>
</div>
<!-- Scripts -->
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', function() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-translate-x-full');
});
// System status check
function checkSystemStatus() {
fetch('/admin/api/system-stats')
.then(response => response.json())
.then(data => {
const indicator = document.getElementById('system-status');
// Simple health check based on CPU and memory
const cpu = data.system.cpu?.percent || 0;
const memory = data.system.memory?.percent || 0;
if (cpu > 90 || memory > 90) {
indicator.className = 'status-indicator status-error';
} else if (cpu > 70 || memory > 70) {
indicator.className = 'status-indicator status-warning';
} else {
indicator.className = 'status-indicator status-healthy';
}
})
.catch(() => {
document.getElementById('system-status').className = 'status-indicator status-error';
});
}
// Check status every 30 seconds
checkSystemStatus();
setInterval(checkSystemStatus, 30000);
// Loading overlay functions
function showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>
ADMIN_BASE_TEMPLATE
# Main dashboard template
cat > backend/app/admin/templates/dashboard/main.html << 'DASHBOARD_MAIN_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<!-- Quick Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- System Health -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-heartbeat text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">System Health</p>
<p class="text-2xl font-bold text-gray-900">
{% if system_stats.cpu.percent < 80 %}Healthy{% else %}Warning{% endif %}
</p>
</div>
</div>
</div>
<!-- Total Users -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<i class="fas fa-users text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Users</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_users or 0 }}</p>
</div>
</div>
</div>
<!-- Certificates -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
<i class="fas fa-certificate text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Certificates</p>
<p class="text-2xl font-bold text-gray-900">{{ db_stats.total_certificates or 0 }}</p>
<p class="text-xs text-gray-500">{{ db_stats.pending_certificates or 0 }} pending</p>
</div>
</div>
</div>
<!-- Blockchain Status -->
<div class="metric-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<i class="fas fa-link text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Blockchain</p>
{% set healthy_containers = fabric_status.values() | selectattr('healthy') | list | length %}
<p class="text-2xl font-bold text-gray-900">
{{ healthy_containers }}/{{ fabric_status | length }}
</p>
<p class="text-xs text-gray-500">containers running</p>
</div>
</div>
</div>
</div>
<!-- System Overview -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- System Resources -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">System Resources</h3>
<button id="refresh-stats" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="space-y-4">
<!-- CPU Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>CPU Usage</span>
<span>{{ "%.1f" | format(system_stats.cpu.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ system_stats.cpu.percent }}%"></div>
</div>
</div>
<!-- Memory Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Memory Usage</span>
<span>{{ "%.1f" | format(system_stats.memory.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full" style="width: {{ system_stats.memory.percent }}%"></div>
</div>
</div>
<!-- Disk Usage -->
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Disk Usage</span>
<span>{{ "%.1f" | format(system_stats.disk.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-orange-600 h-2 rounded-full" style="width: {{ system_stats.disk.percent }}%"></div>
</div>
</div>
</div>
</div>
<!-- Fabric Network Status -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Blockchain Network</h3>
<div class="flex space-x-2">
<button id="start-network" class="px-3 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600">
Start
</button>
<button id="stop-network" class="px-3 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600">
Stop
</button>
</div>
</div>
<div class="space-y-3">
{% for container_name, status in fabric_status.items() %}
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if status.healthy else 'error' }}"></span>
<span class="text-sm font-medium text-gray-700">{{ container_name }}</span>
</div>
<span class="text-xs px-2 py-1 rounded-full {{ 'bg-green-100 text-green-800' if status.healthy else 'bg-red-100 text-red-800' }}">
{{ status.status }}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Pending Certificates -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Pending Certificates</h3>
<a href="{{ url_for('admin.certificates', status='pending') }}"
class="text-sm text-blue-600 hover:text-blue-800">View All</a>
</div>
<div id="pending-certificates" class="space-y-3">
<div class="text-center text-gray-500 py-4">
<i class="fas fa-spinner fa-spin"></i>
Loading...
</div>
</div>
</div>
<!-- Recent Audit Logs -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Activity</h3>
</div>
<div class="space-y-3">
{% for log in recent_logs[:10] %}
<div class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fas fa-user text-blue-600 text-xs"></i>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-900">
<span class="font-medium">{{ log.user.username if log.user else 'System' }}</span>
{{ log.action }}
</p>
<p class="text-xs text-gray-500">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
loadPendingCertificates();
// Network control buttons
document.getElementById('start-network').addEventListener('click', function() {
controlFabricNetwork('start');
});
document.getElementById('stop-network').addEventListener('click', function() {
controlFabricNetwork('stop');
});
// Refresh stats button
document.getElementById('refresh-stats').addEventListener('click', function() {
location.reload();
});
});
function loadPendingCertificates() {
fetch('/admin/certificates/pending')
.then(response => response.json())
.then(data => {
const container = document.getElementById('pending-certificates');
if (data.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-4">No pending certificates</div>';
return;
}
container.innerHTML = data.slice(0, 5).map(cert =>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p class="text-sm font-medium text-gray-900">${cert.product_name}</p>
<p class="text-xs text-gray-500">by ${cert.supplier} • ${cert.days_pending} days ago</p>
</div>
<a href="/admin/certificates/${cert.id}"
class="text-blue-600 hover:text-blue-800 text-sm">Review</a>
</div>
).join('');
});
}
function controlFabricNetwork(action) {
showLoading();
fetch(/admin/blockchain/${action}-network, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert(Network ${action} completed successfully);
setTimeout(() => location.reload(), 2000);
} else {
alert(Failed to ${action} network: ${data.error});
}
})
.catch(error => {
hideLoading();
alert(Error: ${error.message});
});
}
</script>
{% endblock %}
DASHBOARD_MAIN_TEMPLATE
# User list template
cat > backend/app/admin/templates/users/list.html << 'USER_LIST_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}User Management{% endblock %}
{% block content %}
<!-- Header with search and filters -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">User Management</h2>
<div class="flex flex-col sm:flex-row gap-4">
<!-- Search -->
<div class="relative">
<input type="text" id="user-search" placeholder="Search users..."
value="{{ search }}"
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<!-- Role filter -->
<select id="role-filter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Roles</option>
<option value="user" {{ 'selected' if role_filter == 'user' }}>User</option>
<option value="admin" {{ 'selected' if role_filter == 'admin' }}>Admin</option>
<option value="super_admin" {{ 'selected' if role_filter == 'super_admin' }}>Super Admin</option>
</select>
<!-- Create user button -->
<a href="{{ url_for('admin.create_user') }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-plus mr-2"></i>Create User
</a>
</div>
</div>
</div>
<!-- Users table -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Certificates</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for user in users.items %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
{{ user.username[0].upper() }}
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.username }}</div>
<div class="text-sm text-gray-500">{{ user.email }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-purple-100 text-purple-800' if user.role == 'super_admin'
else 'bg-blue-100 text-blue-800' if user.role == 'admin'
else 'bg-gray-100 text-gray-800' }}">
{{ user.role.replace('_', ' ').title() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if user.is_active else 'error' }}"></span>
<span class="text-sm text-gray-900">
{{ 'Active' if user.is_active else 'Inactive' }}
{% if user.is_verified %}
<i class="fas fa-check-circle text-green-500 ml-1" title="Verified"></i>
{% endif %}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ user.certificates.count() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="text-blue-600 hover:text-blue-900">View</a>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | tojson }})"
class="text-{{ 'red' if user.is_active else 'green' }}-600 hover:text-{{ 'red' if user.is_active else 'green' }}-900">
{{ 'Deactivate' if user.is_active else 'Activate' }}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleUserStatus(userId, isActive) {
const action = isActive ? 'deactivate' : 'activate';
if (confirm(Are you sure you want to ${action} this user?)) {
showLoading();
fetch(/admin/users/${userId}/toggle-status, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
USER_LIST_TEMPLATE
# Create integration script
cat > integrate_admin.sh << 'INTEGRATE_ADMIN'
#!/bin/bash
set -e
echo "Integrating Admin Dashboard with Flask App..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# Update main Flask app to register admin blueprint
if ! grep -q "from backend.app.admin import bp as admin_bp" backend/app/__init__.py; then
cat >> backend/app/__init__.py << 'FLASK_ADMIN_INTEGRATION'
# Register admin blueprint
from backend.app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
# Import admin models for database creation
from backend.app.models import admin_models
FLASK_ADMIN_INTEGRATION
fi
# Update User model to include admin functionality
if ! grep -q "role = db.Column" backend/app/models/user.py; then
cat >> backend/app/models/user.py << 'USER_ADMIN_FIELDS'
# Admin fields
role = db.Column(db.String(20), default='user')
is_verified = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True)
last_login = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@property
def is_admin(self):
return self.role in ['admin', 'super_admin']
def has_role(self, role):
return self.role == role
USER_ADMIN_FIELDS
fi
# Update Certificate model with admin fields
if ! grep -q "status = db.Column" backend/app/models/certificate.py; then
cat >> backend/app/models/certificate.py << 'CERT_ADMIN_FIELDS'
# Admin approval fields
status = db.Column(db.String(20), default='pending') # pending, approved, rejected, revoked
approved_at = db.Column(db.DateTime)
approved_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejected_at = db.Column(db.DateTime)
rejected_by = db.Column(db.Integer, db.ForeignKey('users.id'))
rejection_reason = db.Column(db.Text)
revoked_at = db.Column(db.DateTime)
revoked_by = db.Column(db.Integer, db.ForeignKey('users.id'))
revocation_reason = db.Column(db.Text)
# QR Code path
qr_code_path = db.Column(db.String(255))
# Relationships
approver = db.relationship('User', foreign_keys=[approved_by], backref='approved_certificates')
rejecter = db.relationship('User', foreign_keys=[rejected_by], backref='rejected_certificates')
revoker = db.relationship('User', foreign_keys=[revoked_by], backref='revoked_certificates')
def generate_certificate_id(self):
"""Generate unique certificate ID"""
import uuid
import hashlib
unique_string = f"{self.product_name}_{self.user_id}_{uuid.uuid4().hex[:8]}"
self.certificate_id = hashlib.md5(unique_string.encode()).hexdigest()[:12].upper()
return self.certificate_id
CERT_ADMIN_FIELDS
fi
# Install required dependencies
echo "Installing additional dependencies..."
pip install psutil docker
# Create database migration for admin fields
echo "Creating database migration for admin dashboard..."
export FLASK_APP=backend.app:create_app
flask db migrate -m "Add admin dashboard fields"
flask db upgrade
# Create default admin user script
mkdir -p scripts
cat > scripts/create_admin.py << 'CREATE_ADMIN_SCRIPT'
#!/usr/bin/env python3
"""Create default admin user for NanoTrace"""
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend')))
from backend.app import create_app, db
from backend.app.models.user import User
import getpass
def create_admin():
app = create_app()
with app.app_context():
print("Creating NanoTrace Admin User")
print("=" * 30)
username = input("Admin username: ")
email = input("Admin email: ")
password = getpass.getpass("Admin password: ")
# Check if user already exists
existing_user = User.query.filter_by(username=username).first()
if existing_user:
print(f"User {username} already exists!")
return
existing_email = User.query.filter_by(email=email).first()
if existing_email:
print(f"Email {email} already exists!")
return
# Create admin user
admin_user = User(
username=username,
email=email,
role='super_admin',
is_verified=True,
is_active=True
)
admin_user.set_password(password)
db.session.add(admin_user)
db.session.commit()
print(f"Admin user {username} created successfully!")
print(f"You can now login at: http://your-domain/admin/dashboard")
if __name__ == '__main__':
create_admin()
CREATE_ADMIN_SCRIPT
chmod +x scripts/create_admin.py
# Create systemd service monitoring script
cat > scripts/monitor_services.py << 'SERVICE_MONITOR_SCRIPT'
#!/usr/bin/env python3
"""Monitor system services and record metrics"""
import sys
import os
import time
import subprocess
from datetime import datetime
# Add backend to path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend')))
from backend.app import create_app, db
from backend.app.admin.utils import SystemMonitor
def monitor_services():
"""Monitor services and record metrics"""
app = create_app()
with app.app_context():
try:
# Record system metrics
SystemMonitor.record_metrics()
# Check services
services = ['postgresql', 'redis-server', 'nginx', 'nanotrace']
for service in services:
try:
result = subprocess.run(['systemctl', 'is-active', service],
capture_output=True, text=True)
status = result.stdout.strip()
print(f"{datetime.now()}: {service} - {status}")
except Exception as e:
print(f"{datetime.now()}: Error checking {service}: {e}")
print(f"{datetime.now()}: Monitoring cycle completed")
except Exception as e:
print(f"{datetime.now()}: Error in monitoring: {e}")
if __name__ == '__main__':
monitor_services()
SERVICE_MONITOR_SCRIPT
chmod +x scripts/monitor_services.py
# Create cron job for monitoring
echo "Setting up monitoring cron job..."
(crontab -l 2>/dev/null | grep -v nanotrace_monitor; echo "*/5 * * * * cd /home/michal/NanoTrace && /home/michal/NanoTrace/venv/bin/python scripts/monitor_services.py >> /var/log/nanotrace_monitor.log 2>&1") | crontab -
# Create admin CSS
cat > backend/app/admin/static/css/admin.css << 'ADMIN_CSS'
/* Custom admin dashboard styles */
.admin-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.metric-card {
transition: all 0.3s ease;
}
.metric-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.status-card {
border-left: 4px solid;
}
.status-card.healthy {
border-color: #10b981;
}
.status-card.warning {
border-color: #f59e0b;
}
.status-card.error {
border-color: #ef4444;
}
/* Loading animations */
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
/* Table enhancements */
.admin-table tbody tr:hover {
background-color: #f8fafc;
}
/* Chart containers */
.chart-container {
position: relative;
height: 300px;
}
/* Custom scrollbar */
.admin-scroll::-webkit-scrollbar {
width: 6px;
}
.admin-scroll::-webkit-scrollbar-track {
background: #f1f5f9;
}
.admin-scroll::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.admin-scroll::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Mobile responsive fixes */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
}
ADMIN_CSS
echo "Admin Dashboard Integration Complete!"
echo ""
echo "Next steps:"
echo "1. Run: python scripts/create_admin.py (to create admin user)"
echo "2. Restart your Flask application"
echo "3. Access admin dashboard at: http://your-domain/admin/dashboard"
echo "4. Monitor logs at: tail -f /var/log/nanotrace_monitor.log"
echo ""
echo "Admin Features Available:"
echo " 📊 System monitoring with real-time metrics"
echo " 👥 User management (create, edit, activate/deactivate)"
echo " 📄 Certificate approval workflow"
echo " 🔗 Blockchain network control"
echo " ⚙️ System configuration management"
echo " 📋 Audit logging and activity tracking"
echo " 💾 System maintenance utilities"
echo " 🔧 Service control and monitoring"
INTEGRATE_ADMIN
chmod +x integrate_admin.sh
echo "🎛️ Admin Dashboard Builder Complete!"
echo ""
echo "Generated Files:"
echo " 📁 backend/app/admin/ - Admin blueprint with views and templates"
echo " 📊 backend/app/models/admin_models.py - Admin data models"
echo " 🎨 backend/app/admin/templates/ - Professional admin UI templates"
echo " 🔧 scripts/create_admin.py - Admin user creation script"
echo " 📈 scripts/monitor_services.py - System monitoring script"
echo " 🔗 integrate_admin.sh - Integration script"
echo ""
echo "To complete setup, run:"
echo " ./integrate_admin.sh"
echo ""
echo "Admin Dashboard Features:"
echo " 🎯 Real-time system monitoring with charts"
echo " 👤 Complete user management system"
echo " 📋 Certificate approval workflow"
echo " ⛓️ Blockchain network management"
echo " 🔧 System configuration and maintenance"
echo " 📊 Analytics and reporting"
echo " 🔒 Audit logging and security"
echo " 💾 System monitoring and cleanup tools"
echo ""
echo "The dashboard provides a professional admin interface with:"
echo " • Modern responsive design with Tailwind CSS"
echo " • Real-time metrics and monitoring"
echo " • Role-based access control"
echo " • Comprehensive system management"
echo " • Mobile-friendly interface"
echo " • Secure audit logging"ChatGPT said:
Yep—you can run it. Before you do, a couple of quick fixes and preflight checks so it doesn’t blow up mid-run:
Must-do fixes (tiny but important)
-
Import
or_from SQLAlchemy (your views calldb.or_, which doesn’t exist):
-
In
backend/app/admin/views/users.pyandbackend/app/admin/views/certificates.py, add at the top:
pythonfrom sqlalchemy import or_
-
Replace
db.or_(...)withor_(...).
-
User certificates ordering line
Inuser_detailyou have:
pythoncertificates = user.certificates.order_by('created_at desc').limit(10).all()
Use the column expression instead:
pythonfrom backend.app.models.certificate import Certificate
certificates = user.certificates.order_by(Certificate.created_at.desc()).limit(10).all()
Create the missing templates (otherwise routes 500)
Your script only writes:
-
admin_base.html -
dashboard/main.html -
users/list.html -
admin/static/css/admin.css
But the routes also render these (not created yet):
-
backend/app/admin/templates/users/detail.html -
backend/app/admin/templates/users/create.html -
backend/app/admin/templates/certificates/list.html -
backend/app/admin/templates/blockchain/status.html -
backend/app/admin/templates/system/overview.html -
backend/app/admin/templates/system/services.html
If you don’t have designs yet, drop in minimal stubs so pages load:
bashcat > backend/app/admin/templates/users/detail.html <<'HTML'
{% extends "admin_base.html" %}
{% block page_title %}User Detail{% endblock %}
{% block content %}
<h2 class="text-xl font-semibold mb-4">User: {{ user.username }}</h2>
<p>Email: {{ user.email }}</p>
<p>Role: {{ user.role }}</p>
<p>Status: {{ 'Active' if user.is_active else 'Inactive' }}</p>
<h3 class="mt-6 font-semibold">Recent Certificates</h3>
<ul>
{% for c in certificates %}<li>{{ c.product_name }} — {{ c.status }}</li>{% else %}<li>No certificates.</li>{% endfor %}
</ul>
<h3 class="mt-6 font-semibold">Recent Audit Logs</h3>
<ul>
{% for l in audit_logs %}<li>{{ l.timestamp }} — {{ l.action }}</li>{% else %}<li>No logs.</li>{% endfor %}
</ul>
{% endblock %}
HTML
cat > backend/app/admin/templates/users/create.html <<'HTML'
{% extends "admin_base.html" %}
{% block page_title %}Create User{% endblock %}
{% block content %}
<form method="post" class="space-y-4 max-w-md">
<input name="username" placeholder="Username" class="border p-2 w-full" required>
<input name="email" type="email" placeholder="Email" class="border p-2 w-full" required>
<input name="password" type="password" placeholder="Password" class="border p-2 w-full" required>
<select name="role" class="border p-2 w-full">
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="super_admin">Super Admin</option>
</select>
<button class="bg-blue-600 text-white px-4 py-2 rounded">Create</button>
</form>
{% endblock %}
HTML
cat > backend/app/admin/templates/certificates/list.html <<'HTML'
{% extends "admin_base.html" %}
{% block page_title %}Certificates{% endblock %}
{% block content %}
<h2 class="text-xl font-semibold mb-4">Certificates ({{ stats.total }})</h2>
<ul class="space-y-2">
{% for cert in certificates.items %}
<li class="p-3 bg-white border rounded">{{ cert.product_name }} — {{ cert.status }}</li>
{% else %}
<li>No certificates.</li>
{% endfor %}
</ul>
{% endblock %}
HTML
cat > backend/app/admin/templates/blockchain/status.html <<'HTML'
{% extends "admin_base.html" %}
{% block page_title %}Blockchain Status{% endblock %}
{% block content %}
<h2 class="text-xl font-semibold mb-4">Fabric Containers</h2>
<ul>
{% for name, s in fabric_status.items() %}
<li>{{ name }} — {{ s.status }} {{ '(healthy)' if s.healthy else '(down)' }}</li>
{% else %}
<li>No data.</li>
{% endfor %}
</ul>
{% endblock %}
HTML
cat > backend/app/admin/templates/system/overview.html <<'HTML'
{% extends "admin_base.html" %}
{% block page_title %}System Overview{% endblock %}
{% block content %}
<h2 class="text-xl font-semibold mb-4">System Stats</h2>
<pre class="bg-white p-4 border rounded">{{ system_stats | tojson(indent=2) }}</pre>
<h3 class="mt-6 font-semibold">Config</h3>
<pre class="bg-white p-4 border rounded">{{ config_dict | tojson(indent=2) }}</pre>
<h3 class="mt-6 font-semibold">Disk Usage</h3>
<pre class="bg-white p-4 border rounded">{{ disk_usage | tojson(indent=2) }}</pre>
{% endblock %}
HTML
cat > backend/app/admin/templates/system/services.html <<'HTML'
{% extends "admin_base.html" %}
{% block page_title %}System Services{% endblock %}
{% block content %}
<h2 class="text-xl font-semibold mb-4">Services</h2>
<ul>
{% for name, s in services.items() %}
<li>{{ name }} — {{ s.status }}</li>
{% endfor %}
</ul>
{% endblock %}
HTML
Preflight checks
Run these before ./integrate_admin.sh:
bashcd /home/michal/NanoTrace
source venv/bin/activate
# 1) App loads
python - <<'PY'
from backend.app import create_app
app = create_app()
print("App OK:", bool(app))
PY
# 2) Alembic env OK (optional)
export FLASK_APP=backend.app:create_app
flask db current
Run it
bashchmod +x integrate_admin.sh
./integrate_admin.sh
Create a super admin
bashpython scripts/create_admin.py
Restart your service
bashsudo systemctl restart nanotrace sudo systemctl status nanotrace --no-pager
Gotchas to be aware of
-
Docker/Fabric bits: the Start/Stop endpoints expect Docker available and (for start) a script at
/home/michal/NanoTrace/fabric-network/network/scripts/start_network.sh. -
Role field: your
admin_requiredcheckscurrent_user.roleas a string, which matches the addedrole = db.Column(db.String...). All good—just be sure your existing users get a default'user'after the migration (the column default handles new rows; old rows will beNULLuntil you set them). -
Migrations: if
flask db migratefails, runflask db headsand check your env. Worst case, we can generate a manual revision that adds the new columns/tables.
If you want, I can also give you a tiny SQL snippet to backfill role='user' for existing users post-migration.
You said:
#!/bin/bash
# =============================================================================
# NanoTrace Admin Dashboard - Critical Fixes
# Fixes the import errors and missing templates that prevent admin panel from working
# =============================================================================
set -e
echo "Applying critical fixes to Admin Dashboard..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# 1. Fix SQLAlchemy or_ import in user views
echo "Fixing SQLAlchemy import in users.py..."
cat > backend/app/admin/views/users.py << 'FIXED_USER_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from sqlalchemy import or_
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, super_admin_required, log_admin_action
from backend.app.models.user import User
from backend.app import db
from datetime import datetime
from werkzeug.security import generate_password_hash
@bp.route('/users')
@admin_required
def users():
"""User management page"""
log_admin_action("Accessed user management")
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
role_filter = request.args.get('role', '')
query = User.query
if search:
query = query.filter(
or_(
User.username.contains(search),
User.email.contains(search)
)
)
if role_filter:
query = query.filter(User.role == role_filter)
users = query.paginate(
page=page, per_page=25, error_out=False
)
return render_template('users/list.html',
users=users,
search=search,
role_filter=role_filter)
@bp.route('/users/<int:user_id>')
@admin_required
def user_detail(user_id):
"""User detail page"""
from backend.app.models.certificate import Certificate
user = User.query.get_or_404(user_id)
log_admin_action("Viewed user details", "user", user_id)
# Get user's certificates with proper ordering
certificates = user.certificates.order_by(Certificate.created_at.desc()).limit(10).all()
# Get user's audit logs
from backend.app.models.admin_models import AuditLog
audit_logs = AuditLog.query.filter_by(user_id=user_id).order_by(AuditLog.timestamp.desc()).limit(20).all()
return render_template('users/detail.html',
user=user,
certificates=certificates,
audit_logs=audit_logs)
@bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
def toggle_user_status(user_id):
"""Toggle user active status"""
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
try:
db.session.commit()
action = "activated" if user.is_active else "deactivated"
log_admin_action(f"User {action}", "user", user_id)
return jsonify({
'success': True,
'message': f'User {action} successfully',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Create new user"""
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
try:
user = User(
username=data['username'],
email=data['email'],
role=data.get('role', 'user'),
is_verified=data.get('is_verified', False) == 'true',
is_active=data.get('is_active', True) == 'true'
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
log_admin_action("Created user", "user", user.id)
if request.is_json:
return jsonify({
'success': True,
'message': 'User created successfully',
'user_id': user.id
})
else:
flash('User created successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user.id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error creating user: {str(e)}', 'error')
return render_template('users/create.html')
FIXED_USER_VIEWS
# 2. Fix SQLAlchemy or_ import in certificates views
echo "Fixing SQLAlchemy import in certificates.py..."
cat > backend/app/admin/views/certificates.py << 'FIXED_CERT_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from sqlalchemy import or_
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, log_admin_action
from backend.app.models.certificate import Certificate
from backend.app.models.user import User
from backend.app import db
from datetime import datetime, timedelta
@bp.route('/certificates')
@admin_required
def certificates():
"""Certificate management page"""
log_admin_action("Accessed certificate management")
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
search = request.args.get('search', '')
query = Certificate.query
if status_filter:
query = query.filter(Certificate.status == status_filter)
if search:
query = query.join(User).filter(
or_(
Certificate.product_name.contains(search),
Certificate.material_type.contains(search),
User.username.contains(search)
)
)
certificates = query.order_by(
Certificate.created_at.desc()
).paginate(page=page, per_page=25, error_out=False)
# Get statistics
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count() if hasattr(Certificate, 'expiry_date') else 0
}
return render_template('certificates/list.html',
certificates=certificates,
stats=stats,
status_filter=status_filter,
search=search)
@bp.route('/certificates/<int:cert_id>/approve', methods=['POST'])
@admin_required
def approve_certificate(cert_id):
"""Approve certificate"""
certificate = Certificate.query.get_or_404(cert_id)
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'approved'
certificate.approved_at = datetime.utcnow()
certificate.approved_by = current_user.id
# Generate certificate ID if method exists
if hasattr(certificate, 'generate_certificate_id') and not certificate.certificate_id:
certificate.generate_certificate_id()
db.session.commit()
log_admin_action("Approved certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate approved successfully',
'certificate_id': getattr(certificate, 'certificate_id', 'N/A')
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/pending')
@admin_required
def pending_certificates():
"""Get pending certificates for approval queue"""
certificates = Certificate.query.filter_by(
status='pending'
).order_by(Certificate.created_at.asc()).all()
return jsonify([{
'id': cert.id,
'product_name': cert.product_name,
'material_type': cert.material_type,
'supplier': cert.user.username,
'created_at': cert.created_at.isoformat(),
'days_pending': (datetime.utcnow() - cert.created_at).days
} for cert in certificates])
FIXED_CERT_VIEWS
# 3. Create missing templates
echo "Creating missing admin templates..."
# User detail template
cat > backend/app/admin/templates/users/detail.html << 'USER_DETAIL_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}User Detail{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<h2 class="text-xl font-semibold mb-4">User: {{ user.username }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<h3 class="font-semibold text-gray-900 mb-2">Basic Information</h3>
<div class="space-y-2">
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>Role:</strong>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-purple-100 text-purple-800' if user.role == 'super_admin'
else 'bg-blue-100 text-blue-800' if user.role == 'admin'
else 'bg-gray-100 text-gray-800' }}">
{{ user.role.replace('_', ' ').title() }}
</span>
</p>
<p><strong>Status:</strong>
<span class="inline-flex items-center">
<span class="status-indicator status-{{ 'healthy' if user.is_active else 'error' }}"></span>
{{ 'Active' if user.is_active else 'Inactive' }}
{% if user.is_verified %}
<i class="fas fa-check-circle text-green-500 ml-1" title="Verified"></i>
{% endif %}
</span>
</p>
<p><strong>Joined:</strong> {{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'N/A' }}</p>
</div>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Actions</h3>
<div class="space-y-2">
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | tojson }})"
class="block w-full text-left px-4 py-2 bg-{{ 'red' if user.is_active else 'green' }}-500 text-white rounded hover:bg-{{ 'red' if user.is_active else 'green' }}-600">
{{ 'Deactivate User' if user.is_active else 'Activate User' }}
</button>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Certificates ({{ certificates | length }})</h3>
<div class="space-y-3">
{% for cert in certificates %}
<div class="p-3 bg-gray-50 rounded-lg">
<div class="flex justify-between items-start">
<div>
<p class="font-medium text-gray-900">{{ cert.product_name }}</p>
<p class="text-sm text-gray-500">{{ cert.material_type }}</p>
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-green-100 text-green-800' if cert.status == 'approved'
else 'bg-yellow-100 text-yellow-800' if cert.status == 'pending'
else 'bg-red-100 text-red-800' }}">
{{ cert.status.title() }}
</span>
</div>
<p class="text-xs text-gray-500 mt-1">{{ cert.created_at.strftime('%Y-%m-%d') if cert.created_at else 'N/A' }}</p>
</div>
{% else %}
<p class="text-gray-500 text-center py-4">No certificates found.</p>
{% endfor %}
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Activity ({{ audit_logs | length }})</h3>
<div class="space-y-3">
{% for log in audit_logs %}
<div class="p-3 bg-gray-50 rounded-lg">
<p class="text-sm text-gray-900">{{ log.action }}</p>
<p class="text-xs text-gray-500">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') if log.timestamp else 'N/A' }}</p>
</div>
{% else %}
<p class="text-gray-500 text-center py-4">No recent activity.</p>
{% endfor %}
</div>
</div>
</div>
<script>
function toggleUserStatus(userId, isActive) {
const action = isActive ? 'deactivate' : 'activate';
if (confirm(
Are you sure you want to ${action} this user?)) {
showLoading();
fetch(/admin/users/${userId}/toggle-status, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
USER_DETAIL_TEMPLATE
# User create template
cat > backend/app/admin/templates/users/create.html << 'USER_CREATE_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Create User{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h2 class="text-xl font-semibold text-gray-900 mb-6">Create New User</h2>
<form method="post" class="space-y-4 max-w-md">
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<input type="text" id="username" name="username"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input type="email" id="email" name="email"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input type="password" id="password" name="password"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required>
</div>
<div>
<label for="role" class="block text-sm font-medium text-gray-700 mb-1">Role</label>
<select id="role" name="role"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="super_admin">Super Admin</option>
</select>
</div>
<div class="flex items-center">
<input type="checkbox" id="is_verified" name="is_verified" value="true" class="mr-2">
<label for="is_verified" class="text-sm text-gray-700">Verified Account</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="is_active" name="is_active" value="true" checked class="mr-2">
<label for="is_active" class="text-sm text-gray-700">Active Account</label>
</div>
<div class="flex space-x-4">
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
Create User
</button>
<a href="{{ url_for('admin.users') }}"
class="px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors">
Cancel
</a>
</div>
</form>
</div>
{% endblock %}
USER_CREATE_TEMPLATE
# Certificate list template
cat > backend/app/admin/templates/certificates/list.html << 'CERT_LIST_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Certificates{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">Certificate Management</h2>
<div class="flex flex-col sm:flex-row gap-4">
<div class="relative">
<input type="text" id="cert-search" placeholder="Search certificates..."
value="{{ search }}"
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<select id="status-filter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Status</option>
<option value="pending" {{ 'selected' if status_filter == 'pending' }}>Pending</option>
<option value="approved" {{ 'selected' if status_filter == 'approved' }}>Approved</option>
<option value="rejected" {{ 'selected' if status_filter == 'rejected' }}>Rejected</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-6">
<div class="bg-blue-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-certificate text-blue-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-blue-600">Total</p>
<p class="text-2xl font-bold text-blue-900">{{ stats.total }}</p>
</div>
</div>
</div>
<div class="bg-yellow-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-clock text-yellow-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-yellow-600">Pending</p>
<p class="text-2xl font-bold text-yellow-900">{{ stats.pending }}</p>
</div>
</div>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-check text-green-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-green-600">Approved</p>
<p class="text-2xl font-bold text-green-900">{{ stats.approved }}</p>
</div>
</div>
</div>
<div class="bg-red-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-times text-red-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-red-600">Rejected</p>
<p class="text-2xl font-bold text-red-900">{{ stats.rejected }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Product</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Supplier</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for cert in certificates.items %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div>
<div class="text-sm font-medium text-gray-900">{{ cert.product_name }}</div>
<div class="text-sm text-gray-500">{{ cert.material_type }}</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ cert.user.username }}</div>
<div class="text-sm text-gray-500">{{ cert.user.email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-green-100 text-green-800' if cert.status == 'approved'
else 'bg-yellow-100 text-yellow-800' if cert.status == 'pending'
else 'bg-red-100 text-red-800' }}">
{{ cert.status.title() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ cert.created_at.strftime('%Y-%m-%d') if cert.created_at else 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{% if cert.status == 'pending' %}
<button onclick="approveCertificate({{ cert.id }})"
class="text-green-600 hover:text-green-900 mr-3">Approve</button>
{% endif %}
<a href="/admin/certificates/{{ cert.id }}"
class="text-blue-600 hover:text-blue-900">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if certificates.pages > 1 %}
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
{% if certificates.has_prev %}
<a href="{{ url_for('admin.certificates', page=certificates.prev_num, search=search, status=status_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Previous</a>
{% endif %}
{% if certificates.has_next %}
<a href="{{ url_for('admin.certificates', page=certificates.next_num, search=search, status=status_filter) }}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Next</a>
{% endif %}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing <span class="font-medium">{{ certificates.per_page * (certificates.page - 1) + 1 }}</span>
to <span class="font-medium">{{ certificates.per_page * certificates.page if certificates.has_next else certificates.total }}</span>
of <span class="font-medium">{{ certificates.total }}</span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{% for page_num in certificates.iter_pages() %}
{% if page_num %}
{% if page_num != certificates.page %}
<a href="{{ url_for('admin.certificates', page=page_num, search=search, status=status_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">{{ page_num }}</a>
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">{{ page_num }}</span>
{% endif %}
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>
{% endif %}
{% endfor %}
</nav>
</div>
</div>
</div>
{% endif %}
</div>
<script>
function approveCertificate(certId) {
if (confirm('Are you sure you want to approve this certificate?')) {
showLoading();
fetch(/admin/certificates/${certId}/approve, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert('Certificate approved successfully');
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
CERT_LIST_TEMPLATE
# Blockchain status template
cat > backend/app/admin/templates/blockchain/status.html << 'BLOCKCHAIN_STATUS_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Blockchain Status{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">Hyperledger Fabric Network</h2>
<div class="flex space-x-2">
<button id="start-network" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
<i class="fas fa-play mr-2"></i>Start Network
</button>
<button id="stop-network" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
<i class="fas fa-stop mr-2"></i>Stop Network
</button>
<button onclick="location.reload()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
<i class="fas fa-sync-alt mr-2"></i>Refresh
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for name, status in fabric_status.items() %}
<div class="bg-gray-50 p-4 rounded-lg border">
<div class="flex items-center justify-between mb-2">
<h3 class="font-medium text-gray-900">{{ name }}</h3>
<span class="status-indicator status-{{ 'healthy' if status.healthy else 'error' }}"></span>
</div>
<div class="space-y-1">
<p class="text-sm text-gray-600">Status:
<span class="font-medium {{ 'text-green-600' if status.healthy else 'text-red-600' }}">
{{ status.status }}
</span>
</p>
<p class="text-sm text-gray-600">Health:
<span class="font-medium {{ 'text-green-600' if status.healthy else 'text-red-600' }}">
{{ 'Healthy' if status.healthy else 'Unhealthy' }}
</span>
</p>
</div>
</div>
{% else %}
<div class="col-span-full text-center py-8">
<i class="fas fa-exclamation-triangle text-yellow-500 text-3xl mb-4"></i>
<p class="text-gray-600">No Fabric containers found or Docker not accessible.</p>
</div>
{% endfor %}
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Network Information</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-blue-50 p-4 rounded-lg">
<h4 class="font-medium text-blue-900 mb-2">Peer Node</h4>
<p class="text-sm text-blue-700">peer0.org1.nanotrace.com</p>
<p class="text-xs text-blue-600 mt-1">Handles transactions and maintains ledger</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg">
<h4 class="font-medium text-purple-900 mb-2">Orderer Node</h4>
<p class="text-sm text-purple-700">orderer.nanotrace.com</p>
<p class="text-xs text-purple-600 mt-1">Orders transactions into blocks</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<h4 class="font-medium text-green-900 mb-2">Certificate Authority</h4>
<p class="text-sm text-green-700">ca.org1.nanotrace.com</p>
<p class="text-xs text-green-600 mt-1">Manages digital certificates</p>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('start-network').addEventListener('click', function() {
controlFabricNetwork('start');
});
document.getElementById('stop-network').addEventListener('click', function() {
controlFabricNetwork('stop');
});
});
function controlFabricNetwork(action) {
if (!confirm(Are you sure you want to ${action} the Fabric network?)) {
return;
}
showLoading();
fetch(/admin/blockchain/${action}-network, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert(Network ${action} completed successfully);
setTimeout(() => location.reload(), 2000);
} else {
alert(Failed to ${action} network: ${data.error});
}
})
.catch(error => {
hideLoading();
alert(Error: ${error.message});
});
}
</script>
{% endblock %}
BLOCKCHAIN_STATUS_TEMPLATE
# System overview template
cat > backend/app/admin/templates/system/overview.html << 'SYSTEM_OVERVIEW_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}System Overview{% endblock %}
{% block content %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- System Statistics -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Statistics</h3>
<div class="space-y-4">
{% if system_stats.cpu %}
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>CPU Usage</span>
<span>{{ "%.1f" | format(system_stats.cpu.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ system_stats.cpu.percent }}%"></div>
</div>
<p class="text-xs text-gray-500 mt-1">{{ system_stats.cpu.count }} cores available</p>
</div>
{% endif %}
{% if system_stats.memory %}
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Memory Usage</span>
<span>{{ "%.1f" | format(system_stats.memory.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full" style="width: {{ system_stats.memory.percent }}%"></div>
</div>
<p class="text-xs text-gray-500 mt-1">{{ (system_stats.memory.used / 1024**3) | round(1) }} GB / {{ (system_stats.memory.total / 1024**3) | round(1) }} GB</p>
</div>
{% endif %}
{% if system_stats.disk %}
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Disk Usage</span>
<span>{{ "%.1f" | format(system_stats.disk.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-orange-600 h-2 rounded-full" style="width: {{ system_stats.disk.percent }}%"></div>
</div>
<p class="text-xs text-gray-500 mt-1">{{ (system_stats.disk.used / 1024**3) | round(1) }} GB / {{ (system_stats.disk.total / 1024**3) | round(1) }} GB</p>
</div>
{% endif %}
</div>
</div>
<!-- System Configuration -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Configuration</h3>
<div class="space-y-2 max-h-64 overflow-y-auto">
{% for key, value in config_dict.items() %}
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-sm font-medium text-gray-700">{{ key }}</span>
<span class="text-sm text-gray-600 truncate ml-2" title="{{ value }}">{{ value[:50] }}{% if value|length > 50 %}...{% endif %}</span>
</div>
{% else %}
<p class="text-gray-500 text-center py-4">No configuration settings found.</p>
{% endfor %}
</div>
</div>
</div>
<!-- Disk Usage by Directory -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Disk Usage by Directory</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{% for directory, usage in disk_usage.items() %}
{% if usage %}
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium text-gray-900 mb-2">{{ directory }}</h4>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Total:</span>
<span class="font-medium">{{ usage.total_formatted }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Used:</span>
<span class="font-medium">{{ usage.used_formatted }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Free:</span>
<span class="font-medium">{{ usage.free_formatted }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mt-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ (usage.used / usage.total * 100) }}%"></div>
</div>
</div>
</div>
{% else %}
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium text-gray-900 mb-2">{{ directory }}</h4>
<p class="text-sm text-gray-500">Directory not accessible</p>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Raw System Data (for debugging) -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Raw System Data</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 class="font-medium text-gray-700 mb-2">System Stats</h4>
<pre class="bg-gray-50 p-4 rounded text-xs overflow-x-auto">{{ system_stats | tojson(indent=2) }}</pre>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Configuration</h4>
<pre class="bg-gray-50 p-4 rounded text-xs overflow-x-auto">{{ config_dict | tojson(indent=2) }}</pre>
</div>
</div>
</div>
{% endblock %}
SYSTEM_OVERVIEW_TEMPLATE
# System services template
cat > backend/app/admin/templates/system/services.html << 'SYSTEM_SERVICES_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}System Services{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">System Services Status</h2>
<button onclick="location.reload()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
<i class="fas fa-sync-alt mr-2"></i>Refresh Status
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for name, status in services.items() %}
<div class="bg-gray-50 p-4 rounded-lg border">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium text-gray-900">{{ name }}</h3>
<p class="text-sm text-gray-600">System Service</p>
</div>
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if status.active else 'error' }}"></span>
<span class="ml-2 text-sm font-medium {{ 'text-green-600' if status.active else 'text-red-600' }}">
{{ status.status }}
</span>
</div>
</div>
{% if status.error %}
<div class="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
Error: {{ status.error }}
</div>
{% endif %}
</div>
{% else %}
<div class="col-span-full text-center py-8">
<i class="fas fa-exclamation-triangle text-yellow-500 text-3xl mb-4"></i>
<p class="text-gray-600">No service information available.</p>
</div>
{% endfor %}
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Service Descriptions</h3>
<div class="space-y-3">
<div class="border-l-4 border-blue-500 pl-4">
<h4 class="font-medium text-gray-900">PostgreSQL</h4>
<p class="text-sm text-gray-600">Database server for storing application data, user accounts, and certificates.</p>
</div>
<div class="border-l-4 border-red-500 pl-4">
<h4 class="font-medium text-gray-900">Redis Server</h4>
<p class="text-sm text-gray-600">In-memory data structure store used for caching and session management.</p>
</div>
<div class="border-l-4 border-green-500 pl-4">
<h4 class="font-medium text-gray-900">Nginx</h4>
<p class="text-sm text-gray-600">Web server and reverse proxy for serving the application and static files.</p>
</div>
<div class="border-l-4 border-purple-500 pl-4">
<h4 class="font-medium text-gray-900">NanoTrace</h4>
<p class="text-sm text-gray-600">Main Flask application service for the NanoTrace platform.</p>
</div>
</div>
</div>
{% endblock %}
SYSTEM_SERVICES_TEMPLATE
# 4. Run preflight checks
echo "Running preflight checks..."
# Check if app can load
python3 << 'PREFLIGHT_CHECK'
import sys
try:
sys.path.append('/home/michal/NanoTrace/backend')
from backend.app import create_app
app = create_app()
print("✓ App loads successfully")
except Exception as e:
print(f"✗ App loading failed: {e}")
sys.exit(1)
PREFLIGHT_CHECK
# Check Alembic environment
export FLASK_APP=backend.app:create_app
if flask db current &>/dev/null; then
echo "✓ Alembic environment OK"
else
echo "⚠ Alembic environment needs initialization"
fi
echo ""
echo "🎯 Critical fixes applied successfully!"
echo ""
echo "Fixed Issues:"
echo " ✓ SQLAlchemy or_ import added to user and certificate views"
echo " ✓ Certificate ordering fixed to use proper column expression"
echo " ✓ All missing templates created:"
echo " - users/detail.html"
echo " - users/create.html"
echo " - certificates/list.html"
echo " - blockchain/status.html"
echo " - system/overview.html"
echo " - system/services.html"
echo ""
echo "Next steps:"
echo "1. Run: ./integrate_admin.sh"
echo "2. Create admin user: python scripts/create_admin.py"
echo "3. Restart your Flask application"
echo "4. Access admin at: http://localhost:5000/admin/dashboard"
echo ""
echo "All admin routes should now load without 500 errors!"ChatGPT said:
Looks solid—you can run this fixes script. A few quick notes + one missing piece to add before/after you run it:
✅ What your script fixes well
-
Replaces
db.or_withfrom sqlalchemy import or_(good). -
Fixes certificate ordering in
user_detail. -
Creates all previously-missing templates (users, certificates, blockchain, system).
-
Adds a preflight import of
create_appand checks Alembic env. -
Points
admin_base.htmlat your admin CSS viaurl_for('admin.static', ...).
⚠️ One route you still need
Both the dashboard and the certificates list link to /admin/certificates/<id> (“View”), but there isn’t a route to render a single certificate. Add a tiny detail route + template so those links don’t 404:
Route (backend/app/admin/views/certificates.py)
Add this near the bottom:
python@bp.route('/certificates/<int:cert_id>')
@admin_required
def certificate_detail(cert_id):
cert = Certificate.query.get_or_404(cert_id)
log_admin_action("Viewed certificate details", "certificate", cert_id)
return render_template('certificates/detail.html', cert=cert)
Template (backend/app/admin/templates/certificates/detail.html)
html{% extends "admin_base.html" %}
{% block page_title %}Certificate Detail{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Certificate: {{ cert.product_name }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p><strong>Status:</strong> {{ cert.status|title }}</p>
<p><strong>Material:</strong> {{ cert.material_type }}</p>
<p><strong>Created:</strong> {{ cert.created_at.strftime('%Y-%m-%d %H:%M') if cert.created_at else 'N/A' }}</p>
{% if cert.certificate_id %}<p><strong>Certificate ID:</strong> {{ cert.certificate_id }}</p>{% endif %}
</div>
<div>
<p><strong>Supplier:</strong> {{ cert.user.username }}</p>
<p><strong>Email:</strong> {{ cert.user.email }}</p>
{% if cert.approved_at %}<p><strong>Approved At:</strong> {{ cert.approved_at.strftime('%Y-%m-%d %H:%M') }}</p>{% endif %}
</div>
</div>
</div>
{% endblock %}
Tiny cleanups (optional)
-
In
users.pyyou importgenerate_password_hashbut don’t use it (safe to remove). -
The preflight check does
sys.path.append('/home/michal/NanoTrace/backend')and thenfrom backend.app import create_app. That append isn’t strictly needed since you’re already in the project root, but it won’t hurt.
Run it
From your project root:
bashchmod +x <your-fixes-script>.sh
./<your-fixes-script>.sh
# Then integrate (if you haven’t yet)
./integrate_admin.sh
# Create a super admin
python scripts/create_admin.py
# Restart your app service
sudo systemctl restart nanotrace
sudo systemctl status nanotrace --no-pager
If anything errors, paste the traceback and I’ll pinpoint it.
You said:
#!/bin/bash
# =============================================================================
# NanoTrace Admin Dashboard - Critical Fixes
# Fixes the import errors and missing templates that prevent admin panel from working
# =============================================================================
set -e
echo "Applying critical fixes to Admin Dashboard..."
PROJECT_ROOT="/home/michal/NanoTrace"
cd $PROJECT_ROOT
source venv/bin/activate
# 1. Fix SQLAlchemy or_ import in user views
echo "Fixing SQLAlchemy import in users.py..."
cat > backend/app/admin/views/users.py << 'FIXED_USER_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from sqlalchemy import or_
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, super_admin_required, log_admin_action
from backend.app.models.user import User
from backend.app import db
from datetime import datetime
@bp.route('/users')
@admin_required
def users():
"""User management page"""
log_admin_action("Accessed user management")
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
role_filter = request.args.get('role', '')
query = User.query
if search:
query = query.filter(
or_(
User.username.contains(search),
User.email.contains(search)
)
)
if role_filter:
query = query.filter(User.role == role_filter)
users = query.paginate(
page=page, per_page=25, error_out=False
)
return render_template('users/list.html',
users=users,
search=search,
role_filter=role_filter)
@bp.route('/users/<int:user_id>')
@admin_required
def user_detail(user_id):
"""User detail page"""
from backend.app.models.certificate import Certificate
user = User.query.get_or_404(user_id)
log_admin_action("Viewed user details", "user", user_id)
# Get user's certificates with proper ordering
certificates = user.certificates.order_by(Certificate.created_at.desc()).limit(10).all()
# Get user's audit logs
from backend.app.models.admin_models import AuditLog
audit_logs = AuditLog.query.filter_by(user_id=user_id).order_by(AuditLog.timestamp.desc()).limit(20).all()
return render_template('users/detail.html',
user=user,
certificates=certificates,
audit_logs=audit_logs)
@bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
def toggle_user_status(user_id):
"""Toggle user active status"""
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
try:
db.session.commit()
action = "activated" if user.is_active else "deactivated"
log_admin_action(f"User {action}", "user", user_id)
return jsonify({
'success': True,
'message': f'User {action} successfully',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
"""Create new user"""
if request.method == 'POST':
data = request.get_json() if request.is_json else request.form
try:
user = User(
username=data['username'],
email=data['email'],
role=data.get('role', 'user'),
is_verified=data.get('is_verified', False) == 'true',
is_active=data.get('is_active', True) == 'true'
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
log_admin_action("Created user", "user", user.id)
if request.is_json:
return jsonify({
'success': True,
'message': 'User created successfully',
'user_id': user.id
})
else:
flash('User created successfully', 'success')
return redirect(url_for('admin.user_detail', user_id=user.id))
except Exception as e:
db.session.rollback()
if request.is_json:
return jsonify({'success': False, 'error': str(e)}), 400
else:
flash(f'Error creating user: {str(e)}', 'error')
return render_template('users/create.html')
FIXED_USER_VIEWS
# 2. Fix SQLAlchemy or_ import in certificates views
echo "Fixing SQLAlchemy import in certificates.py..."
cat > backend/app/admin/views/certificates.py << 'FIXED_CERT_VIEWS'
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import current_user
from sqlalchemy import or_
from backend.app.admin import bp
from backend.app.admin.utils import admin_required, log_admin_action
from backend.app.models.certificate import Certificate
from backend.app.models.user import User
from backend.app import db
from datetime import datetime, timedelta
@bp.route('/certificates/<int:cert_id>')
@admin_required
def certificate_detail(cert_id):
"""Certificate detail page"""
cert = Certificate.query.get_or_404(cert_id)
log_admin_action("Viewed certificate details", "certificate", cert_id)
return render_template('certificates/detail.html', cert=cert)
@bp.route('/certificates')
@admin_required
def certificates():
"""Certificate management page"""
log_admin_action("Accessed certificate management")
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
search = request.args.get('search', '')
query = Certificate.query
if status_filter:
query = query.filter(Certificate.status == status_filter)
if search:
query = query.join(User).filter(
or_(
Certificate.product_name.contains(search),
Certificate.material_type.contains(search),
User.username.contains(search)
)
)
certificates = query.order_by(
Certificate.created_at.desc()
).paginate(page=page, per_page=25, error_out=False)
# Get statistics
stats = {
'total': Certificate.query.count(),
'pending': Certificate.query.filter_by(status='pending').count(),
'approved': Certificate.query.filter_by(status='approved').count(),
'rejected': Certificate.query.filter_by(status='rejected').count(),
'expired': Certificate.query.filter(
Certificate.expiry_date < datetime.utcnow()
).count() if hasattr(Certificate, 'expiry_date') else 0
}
return render_template('certificates/list.html',
certificates=certificates,
stats=stats,
status_filter=status_filter,
search=search)
@bp.route('/certificates/<int:cert_id>/approve', methods=['POST'])
@admin_required
def approve_certificate(cert_id):
"""Approve certificate"""
certificate = Certificate.query.get_or_404(cert_id)
if certificate.status != 'pending':
return jsonify({
'success': False,
'error': 'Certificate is not pending approval'
}), 400
try:
certificate.status = 'approved'
certificate.approved_at = datetime.utcnow()
certificate.approved_by = current_user.id
# Generate certificate ID if method exists
if hasattr(certificate, 'generate_certificate_id') and not certificate.certificate_id:
certificate.generate_certificate_id()
db.session.commit()
log_admin_action("Approved certificate", "certificate", cert_id)
return jsonify({
'success': True,
'message': 'Certificate approved successfully',
'certificate_id': getattr(certificate, 'certificate_id', 'N/A')
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
@bp.route('/certificates/pending')
@admin_required
def pending_certificates():
"""Get pending certificates for approval queue"""
certificates = Certificate.query.filter_by(
status='pending'
).order_by(Certificate.created_at.asc()).all()
return jsonify([{
'id': cert.id,
'product_name': cert.product_name,
'material_type': cert.material_type,
'supplier': cert.user.username,
'created_at': cert.created_at.isoformat(),
'days_pending': (datetime.utcnow() - cert.created_at).days
} for cert in certificates])
FIXED_CERT_VIEWS
# 3. Create missing templates
echo "Creating missing admin templates..."
# User detail template
cat > backend/app/admin/templates/users/detail.html << 'USER_DETAIL_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}User Detail{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<h2 class="text-xl font-semibold mb-4">User: {{ user.username }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<h3 class="font-semibold text-gray-900 mb-2">Basic Information</h3>
<div class="space-y-2">
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>Role:</strong>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-purple-100 text-purple-800' if user.role == 'super_admin'
else 'bg-blue-100 text-blue-800' if user.role == 'admin'
else 'bg-gray-100 text-gray-800' }}">
{{ user.role.replace('_', ' ').title() }}
</span>
</p>
<p><strong>Status:</strong>
<span class="inline-flex items-center">
<span class="status-indicator status-{{ 'healthy' if user.is_active else 'error' }}"></span>
{{ 'Active' if user.is_active else 'Inactive' }}
{% if user.is_verified %}
<i class="fas fa-check-circle text-green-500 ml-1" title="Verified"></i>
{% endif %}
</span>
</p>
<p><strong>Joined:</strong> {{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'N/A' }}</p>
</div>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Actions</h3>
<div class="space-y-2">
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active | tojson }})"
class="block w-full text-left px-4 py-2 bg-{{ 'red' if user.is_active else 'green' }}-500 text-white rounded hover:bg-{{ 'red' if user.is_active else 'green' }}-600">
{{ 'Deactivate User' if user.is_active else 'Activate User' }}
</button>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Certificates ({{ certificates | length }})</h3>
<div class="space-y-3">
{% for cert in certificates %}
<div class="p-3 bg-gray-50 rounded-lg">
<div class="flex justify-between items-start">
<div>
<p class="font-medium text-gray-900">{{ cert.product_name }}</p>
<p class="text-sm text-gray-500">{{ cert.material_type }}</p>
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-green-100 text-green-800' if cert.status == 'approved'
else 'bg-yellow-100 text-yellow-800' if cert.status == 'pending'
else 'bg-red-100 text-red-800' }}">
{{ cert.status.title() }}
</span>
</div>
<p class="text-xs text-gray-500 mt-1">{{ cert.created_at.strftime('%Y-%m-%d') if cert.created_at else 'N/A' }}</p>
</div>
{% else %}
<p class="text-gray-500 text-center py-4">No certificates found.</p>
{% endfor %}
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Activity ({{ audit_logs | length }})</h3>
<div class="space-y-3">
{% for log in audit_logs %}
<div class="p-3 bg-gray-50 rounded-lg">
<p class="text-sm text-gray-900">{{ log.action }}</p>
<p class="text-xs text-gray-500">{{ log.timestamp.strftime('%Y-%m-%d %H:%M') if log.timestamp else 'N/A' }}</p>
</div>
{% else %}
<p class="text-gray-500 text-center py-4">No recent activity.</p>
{% endfor %}
</div>
</div>
</div>
<script>
function toggleUserStatus(userId, isActive) {
const action = isActive ? 'deactivate' : 'activate';
if (confirm(
Are you sure you want to ${action} this user?)) {
showLoading();
fetch(/admin/users/${userId}/toggle-status, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
USER_DETAIL_TEMPLATE
# User create template
cat > backend/app/admin/templates/users/create.html << 'USER_CREATE_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Create User{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h2 class="text-xl font-semibold text-gray-900 mb-6">Create New User</h2>
<form method="post" class="space-y-4 max-w-md">
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<input type="text" id="username" name="username"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input type="email" id="email" name="email"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input type="password" id="password" name="password"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required>
</div>
<div>
<label for="role" class="block text-sm font-medium text-gray-700 mb-1">Role</label>
<select id="role" name="role"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="super_admin">Super Admin</option>
</select>
</div>
<div class="flex items-center">
<input type="checkbox" id="is_verified" name="is_verified" value="true" class="mr-2">
<label for="is_verified" class="text-sm text-gray-700">Verified Account</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="is_active" name="is_active" value="true" checked class="mr-2">
<label for="is_active" class="text-sm text-gray-700">Active Account</label>
</div>
<div class="flex space-x-4">
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
Create User
</button>
<a href="{{ url_for('admin.users') }}"
class="px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors">
Cancel
</a>
</div>
</form>
</div>
{% endblock %}
USER_CREATE_TEMPLATE
# Certificate detail template
cat > backend/app/admin/templates/certificates/detail.html << 'CERT_DETAIL_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Certificate Detail{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-900">Certificate: {{ cert.product_name }}</h2>
<div class="flex space-x-2">
{% if cert.status == 'pending' %}
<button onclick="approveCertificate({{ cert.id }})"
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
<i class="fas fa-check mr-2"></i>Approve
</button>
<button onclick="rejectCertificate({{ cert.id }})"
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
<i class="fas fa-times mr-2"></i>Reject
</button>
{% endif %}
<a href="{{ url_for('admin.certificates') }}"
class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400">
<i class="fas fa-arrow-left mr-2"></i>Back to List
</a>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<h3 class="font-semibold text-gray-900">Certificate Information</h3>
<div class="space-y-2">
<p><strong>Product Name:</strong> {{ cert.product_name }}</p>
<p><strong>Material Type:</strong> {{ cert.material_type }}</p>
<p><strong>Status:</strong>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-green-100 text-green-800' if cert.status == 'approved'
else 'bg-yellow-100 text-yellow-800' if cert.status == 'pending'
else 'bg-red-100 text-red-800' }}">
{{ cert.status.title() }}
</span>
</p>
<p><strong>Created:</strong> {{ cert.created_at.strftime('%Y-%m-%d %H:%M') if cert.created_at else 'N/A' }}</p>
{% if cert.certificate_id %}
<p><strong>Certificate ID:</strong>
<code class="bg-gray-100 px-2 py-1 rounded text-sm">{{ cert.certificate_id }}</code>
</p>
{% endif %}
{% if hasattr(cert, 'batch_number') and cert.batch_number %}
<p><strong>Batch Number:</strong> {{ cert.batch_number }}</p>
{% endif %}
{% if hasattr(cert, 'quantity') and cert.quantity %}
<p><strong>Quantity:</strong> {{ cert.quantity }}</p>
{% endif %}
</div>
</div>
<div class="space-y-4">
<h3 class="font-semibold text-gray-900">Supplier Information</h3>
<div class="space-y-2">
<p><strong>Supplier:</strong> {{ cert.user.username }}</p>
<p><strong>Email:</strong> {{ cert.user.email }}</p>
<p><strong>User ID:</strong> {{ cert.user.id }}</p>
</div>
{% if cert.status != 'pending' %}
<div class="pt-4 border-t border-gray-200">
<h3 class="font-semibold text-gray-900 mb-2">Approval Information</h3>
<div class="space-y-2">
{% if cert.approved_at %}
<p><strong>Approved At:</strong> {{ cert.approved_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if cert.approver %}
<p><strong>Approved By:</strong> {{ cert.approver.username }}</p>
{% endif %}
{% endif %}
{% if cert.rejected_at %}
<p><strong>Rejected At:</strong> {{ cert.rejected_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if cert.rejecter %}
<p><strong>Rejected By:</strong> {{ cert.rejecter.username }}</p>
{% endif %}
{% if cert.rejection_reason %}
<p><strong>Rejection Reason:</strong></p>
<div class="bg-red-50 border border-red-200 rounded p-3 text-sm">
{{ cert.rejection_reason }}
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
{% if hasattr(cert, 'description') and cert.description %}
<div class="mt-6 pt-6 border-t border-gray-200">
<h3 class="font-semibold text-gray-900 mb-2">Description</h3>
<div class="bg-gray-50 border border-gray-200 rounded p-4">
{{ cert.description }}
</div>
</div>
{% endif %}
{% if hasattr(cert, 'qr_code_path') and cert.qr_code_path %}
<div class="mt-6 pt-6 border-t border-gray-200">
<h3 class="font-semibold text-gray-900 mb-2">QR Code</h3>
<img src="{{ cert.qr_code_path }}" alt="Certificate QR Code" class="w-32 h-32">
</div>
{% endif %}
</div>
<script>
function approveCertificate(certId) {
if (confirm('Are you sure you want to approve this certificate?')) {
showLoading();
fetch(/admin/certificates/${certId}/approve, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert('Certificate approved successfully');
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
function rejectCertificate(certId) {
const reason = prompt('Please provide a reason for rejection:');
if (reason) {
showLoading();
fetch(/admin/certificates/${certId}/reject, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason: reason })
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert('Certificate rejected');
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
CERT_DETAIL_TEMPLATE
{% extends "admin_base.html" %}
{% block page_title %}Certificates{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">Certificate Management</h2>
<div class="flex flex-col sm:flex-row gap-4">
<div class="relative">
<input type="text" id="cert-search" placeholder="Search certificates..."
value="{{ search }}"
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<select id="status-filter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">All Status</option>
<option value="pending" {{ 'selected' if status_filter == 'pending' }}>Pending</option>
<option value="approved" {{ 'selected' if status_filter == 'approved' }}>Approved</option>
<option value="rejected" {{ 'selected' if status_filter == 'rejected' }}>Rejected</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-6">
<div class="bg-blue-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-certificate text-blue-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-blue-600">Total</p>
<p class="text-2xl font-bold text-blue-900">{{ stats.total }}</p>
</div>
</div>
</div>
<div class="bg-yellow-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-clock text-yellow-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-yellow-600">Pending</p>
<p class="text-2xl font-bold text-yellow-900">{{ stats.pending }}</p>
</div>
</div>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-check text-green-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-green-600">Approved</p>
<p class="text-2xl font-bold text-green-900">{{ stats.approved }}</p>
</div>
</div>
</div>
<div class="bg-red-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-times text-red-600 text-xl mr-3"></i>
<div>
<p class="text-sm text-red-600">Rejected</p>
<p class="text-2xl font-bold text-red-900">{{ stats.rejected }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Product</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Supplier</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for cert in certificates.items %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div>
<div class="text-sm font-medium text-gray-900">{{ cert.product_name }}</div>
<div class="text-sm text-gray-500">{{ cert.material_type }}</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ cert.user.username }}</div>
<div class="text-sm text-gray-500">{{ cert.user.email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-green-100 text-green-800' if cert.status == 'approved'
else 'bg-yellow-100 text-yellow-800' if cert.status == 'pending'
else 'bg-red-100 text-red-800' }}">
{{ cert.status.title() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ cert.created_at.strftime('%Y-%m-%d') if cert.created_at else 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{% if cert.status == 'pending' %}
<button onclick="approveCertificate({{ cert.id }})"
class="text-green-600 hover:text-green-900 mr-3">Approve</button>
{% endif %}
<a href="/admin/certificates/{{ cert.id }}"
class="text-blue-600 hover:text-blue-900">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if certificates.pages > 1 %}
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
{% if certificates.has_prev %}
<a href="{{ url_for('admin.certificates', page=certificates.prev_num, search=search, status=status_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Previous</a>
{% endif %}
{% if certificates.has_next %}
<a href="{{ url_for('admin.certificates', page=certificates.next_num, search=search, status=status_filter) }}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">Next</a>
{% endif %}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing <span class="font-medium">{{ certificates.per_page * (certificates.page - 1) + 1 }}</span>
to <span class="font-medium">{{ certificates.per_page * certificates.page if certificates.has_next else certificates.total }}</span>
of <span class="font-medium">{{ certificates.total }}</span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{% for page_num in certificates.iter_pages() %}
{% if page_num %}
{% if page_num != certificates.page %}
<a href="{{ url_for('admin.certificates', page=page_num, search=search, status=status_filter) }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">{{ page_num }}</a>
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">{{ page_num }}</span>
{% endif %}
{% else %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>
{% endif %}
{% endfor %}
</nav>
</div>
</div>
</div>
{% endif %}
</div>
<script>
function approveCertificate(certId) {
if (confirm('Are you sure you want to approve this certificate?')) {
showLoading();
fetch(/admin/certificates/${certId}/approve, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert('Certificate approved successfully');
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
hideLoading();
alert('Error: ' + error.message);
});
}
}
</script>
{% endblock %}
CERT_LIST_TEMPLATE
# Blockchain status template
cat > backend/app/admin/templates/blockchain/status.html << 'BLOCKCHAIN_STATUS_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Blockchain Status{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">Hyperledger Fabric Network</h2>
<div class="flex space-x-2">
<button id="start-network" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
<i class="fas fa-play mr-2"></i>Start Network
</button>
<button id="stop-network" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
<i class="fas fa-stop mr-2"></i>Stop Network
</button>
<button onclick="location.reload()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
<i class="fas fa-sync-alt mr-2"></i>Refresh
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for name, status in fabric_status.items() %}
<div class="bg-gray-50 p-4 rounded-lg border">
<div class="flex items-center justify-between mb-2">
<h3 class="font-medium text-gray-900">{{ name }}</h3>
<span class="status-indicator status-{{ 'healthy' if status.healthy else 'error' }}"></span>
</div>
<div class="space-y-1">
<p class="text-sm text-gray-600">Status:
<span class="font-medium {{ 'text-green-600' if status.healthy else 'text-red-600' }}">
{{ status.status }}
</span>
</p>
<p class="text-sm text-gray-600">Health:
<span class="font-medium {{ 'text-green-600' if status.healthy else 'text-red-600' }}">
{{ 'Healthy' if status.healthy else 'Unhealthy' }}
</span>
</p>
</div>
</div>
{% else %}
<div class="col-span-full text-center py-8">
<i class="fas fa-exclamation-triangle text-yellow-500 text-3xl mb-4"></i>
<p class="text-gray-600">No Fabric containers found or Docker not accessible.</p>
</div>
{% endfor %}
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Network Information</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-blue-50 p-4 rounded-lg">
<h4 class="font-medium text-blue-900 mb-2">Peer Node</h4>
<p class="text-sm text-blue-700">peer0.org1.nanotrace.com</p>
<p class="text-xs text-blue-600 mt-1">Handles transactions and maintains ledger</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg">
<h4 class="font-medium text-purple-900 mb-2">Orderer Node</h4>
<p class="text-sm text-purple-700">orderer.nanotrace.com</p>
<p class="text-xs text-purple-600 mt-1">Orders transactions into blocks</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<h4 class="font-medium text-green-900 mb-2">Certificate Authority</h4>
<p class="text-sm text-green-700">ca.org1.nanotrace.com</p>
<p class="text-xs text-green-600 mt-1">Manages digital certificates</p>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('start-network').addEventListener('click', function() {
controlFabricNetwork('start');
});
document.getElementById('stop-network').addEventListener('click', function() {
controlFabricNetwork('stop');
});
});
function controlFabricNetwork(action) {
if (!confirm(Are you sure you want to ${action} the Fabric network?)) {
return;
}
showLoading();
fetch(/admin/blockchain/${action}-network, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
alert(Network ${action} completed successfully);
setTimeout(() => location.reload(), 2000);
} else {
alert(Failed to ${action} network: ${data.error});
}
})
.catch(error => {
hideLoading();
alert(Error: ${error.message});
});
}
</script>
{% endblock %}
BLOCKCHAIN_STATUS_TEMPLATE
# System overview template
cat > backend/app/admin/templates/system/overview.html << 'SYSTEM_OVERVIEW_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}System Overview{% endblock %}
{% block content %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- System Statistics -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Statistics</h3>
<div class="space-y-4">
{% if system_stats.cpu %}
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>CPU Usage</span>
<span>{{ "%.1f" | format(system_stats.cpu.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ system_stats.cpu.percent }}%"></div>
</div>
<p class="text-xs text-gray-500 mt-1">{{ system_stats.cpu.count }} cores available</p>
</div>
{% endif %}
{% if system_stats.memory %}
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Memory Usage</span>
<span>{{ "%.1f" | format(system_stats.memory.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full" style="width: {{ system_stats.memory.percent }}%"></div>
</div>
<p class="text-xs text-gray-500 mt-1">{{ (system_stats.memory.used / 1024**3) | round(1) }} GB / {{ (system_stats.memory.total / 1024**3) | round(1) }} GB</p>
</div>
{% endif %}
{% if system_stats.disk %}
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Disk Usage</span>
<span>{{ "%.1f" | format(system_stats.disk.percent) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-orange-600 h-2 rounded-full" style="width: {{ system_stats.disk.percent }}%"></div>
</div>
<p class="text-xs text-gray-500 mt-1">{{ (system_stats.disk.used / 1024**3) | round(1) }} GB / {{ (system_stats.disk.total / 1024**3) | round(1) }} GB</p>
</div>
{% endif %}
</div>
</div>
<!-- System Configuration -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Configuration</h3>
<div class="space-y-2 max-h-64 overflow-y-auto">
{% for key, value in config_dict.items() %}
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-sm font-medium text-gray-700">{{ key }}</span>
<span class="text-sm text-gray-600 truncate ml-2" title="{{ value }}">{{ value[:50] }}{% if value|length > 50 %}...{% endif %}</span>
</div>
{% else %}
<p class="text-gray-500 text-center py-4">No configuration settings found.</p>
{% endfor %}
</div>
</div>
</div>
<!-- Disk Usage by Directory -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Disk Usage by Directory</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{% for directory, usage in disk_usage.items() %}
{% if usage %}
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium text-gray-900 mb-2">{{ directory }}</h4>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Total:</span>
<span class="font-medium">{{ usage.total_formatted }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Used:</span>
<span class="font-medium">{{ usage.used_formatted }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Free:</span>
<span class="font-medium">{{ usage.free_formatted }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mt-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: {{ (usage.used / usage.total * 100) }}%"></div>
</div>
</div>
</div>
{% else %}
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium text-gray-900 mb-2">{{ directory }}</h4>
<p class="text-sm text-gray-500">Directory not accessible</p>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Raw System Data (for debugging) -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Raw System Data</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 class="font-medium text-gray-700 mb-2">System Stats</h4>
<pre class="bg-gray-50 p-4 rounded text-xs overflow-x-auto">{{ system_stats | tojson(indent=2) }}</pre>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Configuration</h4>
<pre class="bg-gray-50 p-4 rounded text-xs overflow-x-auto">{{ config_dict | tojson(indent=2) }}</pre>
</div>
</div>
</div>
{% endblock %}
SYSTEM_OVERVIEW_TEMPLATE
# System services template
cat > backend/app/admin/templates/system/services.html << 'SYSTEM_SERVICES_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}System Services{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">System Services Status</h2>
<button onclick="location.reload()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
<i class="fas fa-sync-alt mr-2"></i>Refresh Status
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for name, status in services.items() %}
<div class="bg-gray-50 p-4 rounded-lg border">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium text-gray-900">{{ name }}</h3>
<p class="text-sm text-gray-600">System Service</p>
</div>
<div class="flex items-center">
<span class="status-indicator status-{{ 'healthy' if status.active else 'error' }}"></span>
<span class="ml-2 text-sm font-medium {{ 'text-green-600' if status.active else 'text-red-600' }}">
{{ status.status }}
</span>
</div>
</div>
{% if status.error %}
<div class="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
Error: {{ status.error }}
</div>
{% endif %}
</div>
{% else %}
<div class="col-span-full text-center py-8">
<i class="fas fa-exclamation-triangle text-yellow-500 text-3xl mb-4"></i>
<p class="text-gray-600">No service information available.</p>
</div>
{% endfor %}
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Service Descriptions</h3>
<div class="space-y-3">
<div class="border-l-4 border-blue-500 pl-4">
<h4 class="font-medium text-gray-900">PostgreSQL</h4>
<p class="text-sm text-gray-600">Database server for storing application data, user accounts, and certificates.</p>
</div>
<div class="border-l-4 border-red-500 pl-4">
<h4 class="font-medium text-gray-900">Redis Server</h4>
<p class="text-sm text-gray-600">In-memory data structure store used for caching and session management.</p>
</div>
<div class="border-l-4 border-green-500 pl-4">
<h4 class="font-medium text-gray-900">Nginx</h4>
<p class="text-sm text-gray-600">Web server and reverse proxy for serving the application and static files.</p>
</div>
<div class="border-l-4 border-purple-500 pl-4">
<h4 class="font-medium text-gray-900">NanoTrace</h4>
<p class="text-sm text-gray-600">Main Flask application service for the NanoTrace platform.</p>
</div>
</div>
</div>
{% endblock %}
SYSTEM_SERVICES_TEMPLATE
# 4. Run preflight checks
echo "Running preflight checks..."
# Check if app can load
python3 << 'PREFLIGHT_CHECK'
import sys
try:
from backend.app import create_app
app = create_app()
print("✓ App loads successfully")
except Exception as e:
print(f"✗ App loading failed: {e}")
sys.exit(1)
PREFLIGHT_CHECK
# Check Alembic environment
export FLASK_APP=backend.app:create_app
if flask db current &>/dev/null; then
echo "✓ Alembic environment OK"
else
echo "⚠ Alembic environment needs initialization"
fi
echo ""
echo "🎯 Critical fixes applied successfully!"
echo ""
echo "Fixed Issues:"
echo " ✓ SQLAlchemy or_ import added to user and certificate views"
echo " ✓ Certificate ordering fixed to use proper column expression"
echo " ✓ All missing templates created:"
echo " - users/detail.html"
echo " - users/create.html"
echo " - certificates/list.html"
echo " - blockchain/status.html"
echo " - system/overview.html"
echo " - system/services.html"
echo ""
echo "Next steps:"
echo "1. Run: ./integrate_admin.sh"
echo "2. Create admin user: python scripts/create_admin.py"
echo "3. Restart your Flask application"
echo "4. Access admin at: http://localhost:5000/admin/dashboard"
echo ""
echo "All admin routes should now load without 500 errors!"ChatGPT said:
Great progress! A few critical fixes before you run this script:
-
You accidentally inlined a whole second template (the certificates list page) immediately after closing the
CERT_DETAIL_TEMPLATEheredoc — but without starting a newcat > ... << 'CERT_LIST_TEMPLATE'. As written, lines starting with{% extends "admin_base.html" %}will be executed by bash and the script will bomb out.
Fix that block
Right after the CERT_DETAIL_TEMPLATE terminator, add a new heredoc for list.html:
bash# Certificate detail template
cat > backend/app/admin/templates/certificates/detail.html << 'CERT_DETAIL_TEMPLATE'
... (your existing CERT_DETAIL_TEMPLATE content) ...
{% endblock %}
CERT_DETAIL_TEMPLATE
# Certificate list template
cat > backend/app/admin/templates/certificates/list.html << 'CERT_LIST_TEMPLATE'
... (your existing CERT_LIST_TEMPLATE content) ...
CERT_LIST_TEMPLATE
-
Your
certificates/detail.htmluseshasattr(...)inside Jinja:
jinja{% if hasattr(cert, 'batch_number') and cert.batch_number %}
Jinja doesn’t expose Python’s hasattr. Use the Jinja test instead:
jinja{% if cert.batch_number is defined and cert.batch_number %}
Do the same for description and qr_code_path.
-
You call a reject API from the detail page (
/admin/certificates/<id>/reject) but there’s no route for it. Add this tobackend/app/admin/views/certificates.py:
python@bp.route('/certificates/<int:cert_id>/reject', methods=['POST'])
@admin_required
def reject_certificate(cert_id):
certificate = Certificate.query.get_or_404(cert_id)
if certificate.status != 'pending':
return jsonify({'success': False, 'error': 'Certificate is not pending approval'}), 400
payload = request.get_json(silent=True) or {}
reason = payload.get('reason', '').strip() or request.form.get('reason', '').strip()
try:
certificate.status = 'rejected'
certificate.rejected_at = datetime.utcnow()
certificate.rejected_by = current_user.id
if hasattr(certificate, 'rejection_reason'):
certificate.rejection_reason = reason
db.session.commit()
log_admin_action("Rejected certificate", "certificate", cert_id)
return jsonify({'success': True, 'message': 'Certificate rejected'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
-
Minor tidy-ups (optional):
-
In
users.py, you no longer useflash,redirect, orurl_forin some functions; safe to keep or remove. -
The preflight
from backend.app import create_appassumes you’re running from the project root (which you are). If you ever run it elsewhere, prependsys.path.append('/home/michal/NanoTrace').
Quick checklist before running
-
Split the certificates list template into its own heredoc (see #1).
-
Replace
hasattr(...)withis definedchecks in Jinja (see #2). -
Add the
reject_certificateroute (see #3).
Then run:
bashchmod +x your_fixes_script.sh
./your_fixes_script.sh
./integrate_admin.sh
python scripts/create_admin.py
If anything throws an error, paste it here and I’ll pinpoint the fix.
You said:
cat > backend/app/admin/templates/certificates/detail.html << 'CERT_DETAIL_TEMPLATE'
{% extends "admin_base.html" %}
{% block page_title %}Certificate Detail{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-900">Certificate: {{ cert.product_name }}</h2>
<div class="flex space-x-2">
{% if cert.status == 'pending' %}
<button onclick="approveCertificate({{ cert.id }})"
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"> where to add:# Certificate detail template
cat > backend/app/admin/templates/certificates/detail.html << 'CERT_DETAIL_TEMPLATE'
... (your existing CERT_DETAIL_TEMPLATE content) ...
{% endblock %}
CERT_DETAIL_TEMPLATE
# Certificate list template
cat > backend/app/admin/templates/certificates/list.html << 'CERT_LIST_TEMPLATE'
... (your existing CERT_LIST_TEMPLATE content) ...
CERT_LIST_TEMPLATE
<i class="fas fa-check mr-2"></i>Approve
</button>
<button onclick="rejectCertificate({{ cert.id }})"
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
<i class="fas fa-times mr-2"></i>Reject
</button>
{% endif %}
<a href="{{ url_for('admin.certificates') }}"
class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400">
<i class="fas fa-arrow-left mr-2"></i>Back to List
</a>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<h3 class="font-semibold text-gray-900">Certificate Information</h3>
<div class="space-y-2">
<p><strong>Product Name:</strong> {{ cert.product_name }}</p>
<p><strong>Material Type:</strong> {{ cert.material_type }}</p>
<p><strong>Status:</strong>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ 'bg-green-100 text-green-800' if cert.status == 'approved'
else 'bg-yellow-100 text-yellow-800' if cert.status == 'pending'
else 'bg-red-100 text-red-800' }}">
{{ cert.status.title() }}
</span>
</p>
<p><strong>Created:</strong> {{ cert.created_at.strftime('%Y-%m-%d %H:%M') if cert.created_at else 'N/A' }}</p>
{% if cert.certificate_id %}
<p><strong>Certificate ID:</strong>
<code class="bg-gray-100 px-2 py-1 rounded text-sm">{{ cert.certificate_id }}</code>
</p>
{% endif %}
{% if hasattr(cert, 'batch_number') and cert.batch_number %}
<p><strong>Batch Number:</strong> {{ cert.batch_number }}</p>
{% endif %}
{% if hasattr(cert, 'quantity') and cert.quantity %}
<p><strong>Quantity:</strong> {{ cert.quantity }}</p>
{% endif %}
</div>
</div>
<div class="space-y-4">
<h3 class="font-semibold text-gray-900">Supplier Information</h3>
<div class="space-y-2">
<p><strong>Supplier:</strong> {{ cert.user.username }}</p>
<p><strong>Email:</strong> {{ cert.user.email }}</p>
<p><strong>User ID:</strong> {{ cert.user.id }}</p>
</div>
{% if cert.status != 'pending' %}
<div class="pt-4 border-t border-gray-200">
<h3 class="font-semibold text-gray-900 mb-2">Approval Information</h3>
<div class="space-y-2">
{% if cert.approved_at %}
<p><strong>Approved At:</strong> {{ cert.approved_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if cert.approver %}
<p><strong>Approved By:</strong> {{ cert.approver.username }}</p>
{% endif %}
{% endif %}
{% if cert.rejected_at %}
<p><strong>Rejected At:</strong> {{ cert.rejected_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if cert.rejecter %}
<p><strong>Rejected By:</strong> {{ cert.rejecter.username }}</p>
{% endif %}
{% if cert.rejection_reason %}
<p><strong>Rejection Reason:</strong></p>
<div class="bg-red-50 border border-red-200 rounded p-3 text-sm">
{{ cert.rejection_reason }}
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
{% if hasattr(cert, 'description') and cert.description %}
<div class="mt-6 pt-6 border-t border-gray-200">
<h3 class="font-semibold text-gray-900 mb-2">Description</h3>
<div class="bg-gray-50 border border-gray-200 rounded p-4">
{{ cert.description }}
</div>
</div>
{% endif %}
{% if hasattr(cert, 'qr_code_path') and cert.qr_code_path %}
<div class="mt-6 pt-6 border-t border-gray-200">
<h3 class="font-semibold text-gray-900 mb-2">QR Code</h3>
<img src="{{ cert.qr_code_path }}" alt="Certificate QR Code" class="w-32 h-32">
</div>
{% endif %}
</div>
ChatGPT said:
You accidentally pasted shell heredoc lines inside the Jinja template. Those cat > ... << 'CERT_DETAIL_TEMPLATE' and CERT_LIST_TEMPLATE markers belong in your bash script, not inside detail.html.
Do this:
1) In your bash script
Put the two heredocs one after the other, with nothing Jinja-related between the terminator of the first and the start of the second:
bash# Certificate detail template
cat > backend/app/admin/templates/certificates/detail.html << 'CERT_DETAIL_TEMPLATE'
<!-- full Jinja/HTML for detail.html goes here (see section 2) -->
CERT_DETAIL_TEMPLATE
# Certificate list template
cat > backend/app/admin/templates/certificates/list.html << 'CERT_LIST_TEMPLATE'
<!-- full Jinja/HTML for list.html -->
CERT_LIST_TEMPLATE
2) In detail.html (the file content itself)
Make sure it contains only Jinja/HTML—no shell lines. Also replace hasattr(...) (not available in Jinja) with the Jinja test is defined.
Here’s your corrected backend/app/admin/templates/certificates/detail.html:
jinja{% extends "admin_base.html" %} {% block page_title %}Certificate Detail{% endblock %} {% block content %} <div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200"> <div class="flex items-center justify-between mb-6"> <h2 class="text-xl font-semibold text-gray-900">Certificate: {{ cert.product_name }}</h2> <div class="flex space-x-2"> {% if cert.status == 'pending' %} <button onclick="approveCertificate({{ cert.id }})" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"> <i class="fas fa-check mr-2"></i>Approve </button> <button onclick="rejectCertificate({{ cert.id }})" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"> <i class="fas fa-times mr-2"></i>Reject </button> {% endif %} <a href="{{ url_for('admin.certificates') }}" class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"> <i class="fas fa-arrow-left mr-2"></i>Back to List </a> </div> </div> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="space-y-4"> <h3 class="font-semibold text-gray-900">Certificate Information</h3> <div class="space-y-2"> <p><strong>Product Name:</strong> {{ cert.product_name }}</p> <p><strong>Material Type:</strong> {{ cert.material_type }}</p> <p><strong>Status:</strong> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ 'bg-green-100 text-green-800' if cert.status == 'approved' else 'bg-yellow-100 text-yellow-800' if cert.status == 'pending' else 'bg-red-100 text-red-800' }}"> {{ cert.status.title() }} </span> </p> <p><strong>Created:</strong> {{ cert.created_at.strftime('%Y-%m-%d %H:%M') if cert.created_at else 'N/A' }}</p> {% if cert.certificate_id %} <p><strong>Certificate ID:</strong> <code class="bg-gray-100 px-2 py-1 rounded text-sm">{{ cert.certificate_id }}</code> </p> {% endif %} {% if cert.batch_number is defined and cert.batch_number %} <p><strong>Batch Number:</strong> {{ cert.batch_number }}</p> {% endif %} {% if cert.quantity is defined and cert.quantity %} <p><strong>Quantity:</strong> {{ cert.quantity }}</p> {% endif %} </div> </div> <div class="space-y-4"> <h3 class="font-semibold text-gray-900">Supplier Information</h3> <div class="space-y-2"> <p><strong>Supplier:</strong> {{ cert.user.username }}</p> <p><strong>Email:</strong> {{ cert.user.email }}</p> <p><strong>User ID:</strong> {{ cert.user.id }}</p> </div> {% if cert.status != 'pending' %} <div class="pt-4 border-t border-gray-200"> <h3 class="font-semibold text-gray-900 mb-2">Approval Information</h3> <div class="space-y-2"> {% if cert.approved_at %} <p><strong>Approved At:</strong> {{ cert.approved_at.strftime('%Y-%m-%d %H:%M') }}</p> {% if cert.approver %} <p><strong>Approved By:</strong> {{ cert.approver.username }}</p> {% endif %} {% endif %} {% if cert.rejected_at %} <p><strong>Rejected At:</strong> {{ cert.rejected_at.strftime('%Y-%m-%d %H:%M') }}</p> {% if cert.rejecter %} <p><strong>Rejected By:</strong> {{ cert.rejecter.username }}</p> {% endif %} {% if cert.rejection_reason %} <p><strong>Rejection Reason:</strong></p> <div class="bg-red-50 border border-red-200 rounded p-3 text-sm"> {{ cert.rejection_reason }} </div> {% endif %} {% endif %} </div> </div> {% endif %} </div> </div> {% if cert.description is defined and cert.description %} <div class="mt-6 pt-6 border-t border-gray-200"> <h3 class="font-semibold text-gray-900 mb-2">Description</h3> <div class="bg-gray-50 border border-gray-200 rounded p-4"> {{ cert.description }} </div> </div> {% endif %} {% if cert.qr_code_path is defined and cert.qr_code_path %} <div class="mt-6 pt-6 border-t border-gray-200"> <h3 class="font-semibold text-gray-900 mb-2">QR Code</h3> <img src="{{ cert.qr_code_path }}" alt="Certificate QR Code" class="w-32 h-32"> </div> {% endif %} </div> <script> function approveCertificate(certId) { if (!confirm('Approve this certificate?')) return; showLoading(); fetch(`/admin/certificates/${certId}/approve`, { method: 'POST', headers: {'Content-Type':'application/json'} }) .then(r => r.json()).then(d => { hideLoading(); d.success ? location.reload() : alert('Error: ' + d.error); }) .catch(e => { hideLoading(); alert('Error: ' + e.message); }); } function rejectCertificate(certId) { const reason = prompt('Reason for rejection:'); if (!reason) return; showLoading(); fetch(`/admin/certificates/${certId}/reject`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ reason }) }) .then(r => r.json()).then(d => { hideLoading(); d.success ? location.reload() : alert('Error: ' + d.error); }) .catch(e => { hideLoading(); alert('Error: ' + e.message); }); } </script> {% endblock %}
That’s it — keep the heredoc wrappers in the script, and the pure template in the file content. If you still see errors, paste the exact bash error or Jinja traceback and I’ll zero in on it.
ChatGPT can make mistakes. Check important info. See Cookie Preferences.
